From 36fd3ac6c12b3b786c5a7d8fa958b523a7af111c Mon Sep 17 00:00:00 2001 From: Maru Newby Date: Fri, 2 Mar 2012 15:44:51 -0800 Subject: [PATCH] Add retry support to the quantum client. * Based on the retry support in nova's melange_connection and glance * Retry configuration is defined during Client instantiation and only applied to idempotent GET requests * Updated all api methods to call http method helpers (delete/get/post/put) rather than do_request to ensure consistent behavoir for a given http method. * Fixed bug in quantum.common.exceptions.QuantumClientException that was unnecessarily overriding class's 'message' attribute. * Resolves bug 937379 Change-Id: Iab4e2ccf97937502ee0df58dba1e2dca30a36df8 --- .gitignore | 2 +- quantum/client/__init__.py | 114 +++++++++++++------- quantum/client/tests/unit/test_clientlib.py | 11 ++ quantum/common/exceptions.py | 8 +- 4 files changed, 94 insertions(+), 41 deletions(-) diff --git a/.gitignore b/.gitignore index 2bd0bd873..1fe7c256e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ build/* build-stamp .coverage cover/* -quantum_client.egg-info/* +python_quantumclient.egg-info/* quantum/vcsversion.py run_tests.err.log run_tests.log diff --git a/quantum/client/__init__.py b/quantum/client/__init__.py index 7b43b977c..9c91fe117 100644 --- a/quantum/client/__init__.py +++ b/quantum/client/__init__.py @@ -19,6 +19,7 @@ import logging import httplib import socket +import time import urllib from quantum.common import exceptions @@ -170,10 +171,12 @@ class Client(object): detail_path = "/detail" def __init__(self, host="127.0.0.1", port=9696, use_ssl=False, tenant=None, - format="xml", testingStub=None, key_file=None, cert_file=None, - auth_token=None, logger=None, - version="1.1", - uri_prefix="/tenants/{tenant_id}"): + format="xml", testingStub=None, key_file=None, cert_file=None, + auth_token=None, logger=None, + version="1.1", + uri_prefix="/tenants/{tenant_id}", + retries=0, + retry_interval=1): """ Creates a new client to some service. @@ -188,6 +191,8 @@ def __init__(self, host="127.0.0.1", port=9696, use_ssl=False, tenant=None, :param auth_token: authentication token to be passed to server :param logger: Logger object for the client library :param action_prefix: prefix for request URIs + :param retries: How many times to retry a failed connection attempt + :param retry_interval: The # of seconds between connection attempts """ self.host = host self.port = port @@ -202,6 +207,8 @@ def __init__(self, host="127.0.0.1", port=9696, use_ssl=False, tenant=None, self.auth_token = auth_token self.version = version self.action_prefix = "/v%s%s" % (version, uri_prefix) + self.retries = retries + self.retry_interval = retry_interval def _handle_fault_response(self, status_code, response_body): # Create exception with HTTP status code and message @@ -253,7 +260,7 @@ def do_request(self, method, action, body=None, :param headers: mapping of key/value pairs to add as headers :param params: dictionary of key/value pairs to add to append to action - + :raises: ConnectionFailed """ LOG.debug("Client issuing request: %s", action) # Ensure we have a tenant id @@ -296,9 +303,9 @@ def do_request(self, method, action, body=None, else: self._handle_fault_response(status_code, data) except (socket.error, IOError), e: - msg = "Unable to connect to server. Got error: %s" % e - LOG.exception(msg) - raise Exception(msg) + exc = exceptions.ConnectionFailed(reason=str(e)) + LOG.exception(exc.message) + raise exc def get_status_code(self, response): """ @@ -341,130 +348,162 @@ def content_type(self, format=None): format = self.format return "application/%s" % (format) + def retry_request(self, method, action, body=None, + headers=None, params=None): + """ + Call do_request with the default retry configuration. Only + idempotent requests should retry failed connection attempts. + + :raises: ConnectionFailed if the maximum # of retries is exceeded + """ + max_attempts = self.retries + 1 + for i in xrange(max_attempts): + try: + return self.do_request(method, action, body=body, + headers=headers, params=params) + except exceptions.ConnectionFailed: + # Exception has already been logged by do_request() + if i < self.retries: + LOG.debug(_('Retrying connection to quantum service')) + time.sleep(self.retry_interval) + + raise exceptions.ConnectionFailed(reason=_("Maximum attempts reached")) + + def delete(self, action, body=None, headers=None, params=None): + return self.retry_request("DELETE", action, body=body, + headers=headers, params=params) + + def get(self, action, body=None, headers=None, params=None): + return self.retry_request("GET", action, body=body, + headers=headers, params=params) + + def post(self, action, body=None, headers=None, params=None): + # Do not retry POST requests to avoid the orphan objects problem. + return self.do_request("POST", action, body=body, + headers=headers, params=params) + + def put(self, action, body=None, headers=None, params=None): + return self.retry_request("PUT", action, body=body, + headers=headers, params=params) + @ApiCall def list_networks(self): """ Fetches a list of all networks for a tenant """ - return self.do_request("GET", self.networks_path) + return self.get(self.networks_path) @ApiCall def list_networks_details(self): """ Fetches a detailed list of all networks for a tenant """ - return self.do_request("GET", self.networks_path + self.detail_path) + return self.get(self.networks_path + self.detail_path) @ApiCall def show_network(self, network): """ Fetches information of a certain network """ - return self.do_request("GET", self.network_path % (network)) + return self.get(self.network_path % (network)) @ApiCall def show_network_details(self, network): """ Fetches the details of a certain network """ - return self.do_request("GET", (self.network_path + self.detail_path) - % (network)) + return self.get((self.network_path + self.detail_path) % (network)) @ApiCall def create_network(self, body=None): """ Creates a new network """ - return self.do_request("POST", self.networks_path, body=body) + return self.post(self.networks_path, body=body) @ApiCall def update_network(self, network, body=None): """ Updates a network """ - return self.do_request("PUT", self.network_path % (network), body=body) + return self.put(self.network_path % (network), body=body) @ApiCall def delete_network(self, network): """ Deletes the specified network """ - return self.do_request("DELETE", self.network_path % (network)) + return self.delete(self.network_path % (network)) @ApiCall def list_ports(self, network): """ Fetches a list of ports on a given network """ - return self.do_request("GET", self.ports_path % (network)) + return self.get(self.ports_path % (network)) @ApiCall def list_ports_details(self, network): """ Fetches a detailed list of ports on a given network """ - return self.do_request("GET", (self.ports_path + self.detail_path) - % (network)) + return self.get((self.ports_path + self.detail_path) % (network)) @ApiCall def show_port(self, network, port): """ Fetches the information of a certain port """ - return self.do_request("GET", self.port_path % (network, port)) + return self.get(self.port_path % (network, port)) @ApiCall def show_port_details(self, network, port): """ Fetches the details of a certain port """ - return self.do_request("GET", (self.port_path + self.detail_path) - % (network, port)) + return self.get((self.port_path + self.detail_path) % (network, port)) @ApiCall def create_port(self, network, body=None): """ Creates a new port on a given network """ - return self.do_request("POST", self.ports_path % (network), body=body) + return self.post(self.ports_path % (network), body=body) @ApiCall def delete_port(self, network, port): """ Deletes the specified port from a network """ - return self.do_request("DELETE", self.port_path % (network, port)) + return self.delete(self.port_path % (network, port)) @ApiCall def update_port(self, network, port, body=None): """ Sets the attributes of the specified port """ - return self.do_request("PUT", - self.port_path % (network, port), body=body) + return self.put(self.port_path % (network, port), body=body) @ApiCall def show_port_attachment(self, network, port): """ Fetches the attachment-id associated with the specified port """ - return self.do_request("GET", self.attachment_path % (network, port)) + return self.get(self.attachment_path % (network, port)) @ApiCall def attach_resource(self, network, port, body=None): """ Sets the attachment-id of the specified port """ - return self.do_request("PUT", - self.attachment_path % (network, port), body=body) + return self.put(self.attachment_path % (network, port), body=body) @ApiCall def detach_resource(self, network, port): """ Removes the attachment-id of the specified port """ - return self.do_request("DELETE", - self.attachment_path % (network, port)) + return self.delete(self.attachment_path % (network, port)) class ClientV11(Client): @@ -479,15 +518,14 @@ def list_networks(self, **filters): Fetches a list of all networks for a tenant """ # Pass filters in "params" argument to do_request - return self.do_request("GET", self.networks_path, params=filters) + return self.get(self.networks_path, params=filters) @ApiCall def list_networks_details(self, **filters): """ Fetches a detailed list of all networks for a tenant """ - return self.do_request("GET", self.networks_path + self.detail_path, - params=filters) + return self.get(self.networks_path + self.detail_path, params=filters) @ApiCall def list_ports(self, network, **filters): @@ -495,14 +533,12 @@ def list_ports(self, network, **filters): Fetches a list of ports on a given network """ # Pass filters in "params" argument to do_request - return self.do_request("GET", self.ports_path % (network), - params=filters) + return self.get(self.ports_path % (network), params=filters) @ApiCall def list_ports_details(self, network, **filters): """ Fetches a detailed list of ports on a given network """ - return self.do_request("GET", (self.ports_path + self.detail_path) - % (network), - params=filters) + return self.get((self.ports_path + self.detail_path) % (network), + params=filters) diff --git a/quantum/client/tests/unit/test_clientlib.py b/quantum/client/tests/unit/test_clientlib.py index f198d2a43..33f094526 100644 --- a/quantum/client/tests/unit/test_clientlib.py +++ b/quantum/client/tests/unit/test_clientlib.py @@ -20,6 +20,7 @@ import unittest import re +from quantum.common import exceptions from quantum.common.serializer import Serializer from quantum.client import Client @@ -755,3 +756,13 @@ def test_detach_resource_error_430(self): def test_ssl_certificates(self): self._test_ssl_certificates() + + def test_connection_retry_failure(self): + self.client = Client(port=55555, tenant=TENANT_1, retries=1, + retry_interval=0) + try: + self.client.list_networks() + except exceptions.ConnectionFailed as exc: + self.assertTrue('Maximum attempts reached' in str(exc)) + else: + self.fail('ConnectionFailed not raised') diff --git a/quantum/common/exceptions.py b/quantum/common/exceptions.py index 043d4f1ed..69b7a3f6f 100644 --- a/quantum/common/exceptions.py +++ b/quantum/common/exceptions.py @@ -90,7 +90,9 @@ class AlreadyAttached(QuantumException): class QuantumClientException(QuantumException): def __init__(self, **kwargs): - self.message = kwargs.get('message', "") + message = kwargs.get('message') + if message: + self.message = message super(QuantumClientException, self).__init__(**kwargs) @@ -134,6 +136,10 @@ class QuantumCLIError(QuantumClientException): pass +class ConnectionFailed(QuantumClientException): + message = _("Connection to quantum failed: %(reason)s") + + class BadInputError(Exception): """Error resulting from a client sending bad input to a server""" pass