Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Maru Newby committed Mar 15, 2012
1 parent 46783f1 commit 36fd3ac
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 41 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Expand Up @@ -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
Expand Down
114 changes: 75 additions & 39 deletions quantum/client/__init__.py
Expand Up @@ -19,6 +19,7 @@
import logging
import httplib
import socket
import time
import urllib

from quantum.common import exceptions
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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):
Expand All @@ -479,30 +518,27 @@ 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):
"""
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)
11 changes: 11 additions & 0 deletions quantum/client/tests/unit/test_clientlib.py
Expand Up @@ -20,6 +20,7 @@
import unittest
import re

from quantum.common import exceptions
from quantum.common.serializer import Serializer
from quantum.client import Client

Expand Down Expand Up @@ -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')
8 changes: 7 additions & 1 deletion quantum/common/exceptions.py
Expand Up @@ -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)


Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 36fd3ac

Please sign in to comment.