620 lines
22 KiB
Python
620 lines
22 KiB
Python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
# Copyright 2011 Citrix Systems
|
|
# 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.
|
|
# @author: Tyler Smith, Cisco Systems
|
|
|
|
import gettext
|
|
import httplib
|
|
import logging
|
|
import socket
|
|
import time
|
|
import urllib
|
|
|
|
|
|
# gettext must be initialized before any quantumclient imports
|
|
gettext.install('quantumclient', unicode=1)
|
|
|
|
|
|
from quantumclient.common import exceptions
|
|
from quantumclient.common.serializer import Serializer
|
|
from quantumclient.common import utils
|
|
|
|
net_filters_v11_opt = []
|
|
net_filters_v11_opt.append({'--name':
|
|
{'help': _('name filter'), }, })
|
|
net_filters_v11_opt.append({'--op-status':
|
|
{'help': _('op-status filter'), }, })
|
|
net_filters_v11_opt.append({'--port-op-status':
|
|
{'help': _('port-op-status filter'), }, })
|
|
net_filters_v11_opt.append({'--port-state':
|
|
{'help': _('port-state filter'), }, })
|
|
net_filters_v11_opt.append({'--has-attachment':
|
|
{'help': _('has-attachment filter'), }, })
|
|
net_filters_v11_opt.append({'--attachment':
|
|
{'help': _('attachment filter'), }, })
|
|
net_filters_v11_opt.append({'--port':
|
|
{'help': _('port filter'), }, })
|
|
|
|
port_filters_v11_opt = []
|
|
port_filters_v11_opt.append({'--name':
|
|
{'help': _('name filter'), }, })
|
|
port_filters_v11_opt.append({'--op-status':
|
|
{'help': _('op-status filter'), }, })
|
|
port_filters_v11_opt.append({'--has-attachment':
|
|
{'help': _('has-attachment filter'), }, })
|
|
port_filters_v11_opt.append({'--attachement':
|
|
{'help': _('attachment filter'), }, })
|
|
|
|
net_filters_v11 = []
|
|
for arg in net_filters_v11_opt:
|
|
for key in arg.iterkeys():
|
|
net_filters_v11.append(key.split('--', 2)[1])
|
|
|
|
port_filters_v11 = []
|
|
for arg in port_filters_v11_opt:
|
|
for key in arg.iterkeys():
|
|
port_filters_v11.append(key.split('--', 2)[1])
|
|
|
|
LOG = logging.getLogger('quantumclient')
|
|
|
|
AUTH_TOKEN_HEADER = "X-Auth-Token"
|
|
|
|
|
|
def exception_handler_v10(status_code, error_content):
|
|
""" Exception handler for API v1.0 client
|
|
|
|
This routine generates the appropriate
|
|
Quantum exception according to the contents of the
|
|
response body
|
|
|
|
:param status_code: HTTP error status code
|
|
:param error_content: deserialized body of error response
|
|
"""
|
|
|
|
quantum_error_types = {
|
|
420: 'networkNotFound',
|
|
421: 'networkInUse',
|
|
430: 'portNotFound',
|
|
431: 'requestedStateInvalid',
|
|
432: 'portInUse',
|
|
440: 'alreadyAttached', }
|
|
|
|
quantum_errors = {
|
|
400: exceptions.BadInputError,
|
|
401: exceptions.Unauthorized,
|
|
404: exceptions.NotFound,
|
|
420: exceptions.NetworkNotFoundClient,
|
|
421: exceptions.NetworkInUseClient,
|
|
430: exceptions.PortNotFoundClient,
|
|
431: exceptions.StateInvalidClient,
|
|
432: exceptions.PortInUseClient,
|
|
440: exceptions.AlreadyAttachedClient,
|
|
501: NotImplementedError, }
|
|
|
|
# Find real error type
|
|
error_type = None
|
|
if isinstance(error_content, dict):
|
|
error_type = quantum_error_types.get(status_code)
|
|
if error_type:
|
|
error_dict = error_content[error_type]
|
|
error_message = error_dict['message'] + "\n" + error_dict['detail']
|
|
else:
|
|
error_message = error_content
|
|
# raise the appropriate error!
|
|
ex = quantum_errors[status_code](message=error_message)
|
|
ex.args = ([dict(status_code=status_code,
|
|
message=error_message)],)
|
|
raise ex
|
|
|
|
|
|
def exception_handler_v11(status_code, error_content):
|
|
""" Exception handler for API v1.1 client
|
|
|
|
This routine generates the appropriate
|
|
Quantum exception according to the contents of the
|
|
response body
|
|
|
|
:param status_code: HTTP error status code
|
|
:param error_content: deserialized body of error response
|
|
"""
|
|
|
|
quantum_errors = {
|
|
'NetworkNotFound': exceptions.NetworkNotFoundClient,
|
|
'NetworkInUse': exceptions.NetworkInUseClient,
|
|
'PortNotFound': exceptions.PortNotFoundClient,
|
|
'RequestedStateInvalid': exceptions.StateInvalidClient,
|
|
'PortInUse': exceptions.PortInUseClient,
|
|
'AlreadyAttached': exceptions.AlreadyAttachedClient,
|
|
'QuantumServiceFault': exceptions.QuantumClientException, }
|
|
|
|
error_dict = None
|
|
if isinstance(error_content, dict):
|
|
error_dict = error_content.get('QuantumError')
|
|
# Find real error type
|
|
if error_dict:
|
|
# If QuantumError key is found, it will definitely contain
|
|
# a 'message' and 'type' keys
|
|
error_type = error_dict['type']
|
|
error_message = (error_dict['message'] + "\n" +
|
|
error_dict['detail'])
|
|
# raise the appropriate error!
|
|
ex = quantum_errors[error_type](message=error_message)
|
|
ex.args = ([dict(status_code=status_code,
|
|
message=error_message)],)
|
|
raise ex
|
|
# If we end up here the exception was not a quantum error
|
|
msg = "%s-%s" % (status_code, error_content)
|
|
raise exceptions.QuantumClientException(message=msg)
|
|
|
|
|
|
EXCEPTION_HANDLERS = {
|
|
'1.0': exception_handler_v10,
|
|
'1.1': exception_handler_v11,
|
|
}
|
|
|
|
|
|
class ApiCall(object):
|
|
"""A Decorator to add support for format and tenant overriding"""
|
|
def __init__(self, function):
|
|
self.function = function
|
|
|
|
def __get__(self, instance, owner):
|
|
def with_params(*args, **kwargs):
|
|
"""
|
|
Temporarily sets the format and tenant for this request
|
|
"""
|
|
(format, tenant) = (instance.format, instance.tenant)
|
|
|
|
if 'format' in kwargs:
|
|
instance.format = kwargs['format']
|
|
if 'tenant' in kwargs:
|
|
instance.tenant = kwargs['tenant']
|
|
|
|
ret = self.function(instance, *args)
|
|
(instance.format, instance.tenant) = (format, tenant)
|
|
return ret
|
|
return with_params
|
|
|
|
|
|
class APIFilterCall(object):
|
|
|
|
def __init__(self, filters):
|
|
self.filters = filters
|
|
|
|
def __call__(self, f):
|
|
def wrapped_f(*args, **kwargs):
|
|
"""
|
|
Temporarily sets the format and tenant for this request
|
|
"""
|
|
instance = args[0]
|
|
(format, tenant) = (instance.format, instance.tenant)
|
|
|
|
if 'format' in kwargs:
|
|
instance.format = kwargs['format']
|
|
if 'format' not in self.filters:
|
|
del kwargs['format']
|
|
if 'tenant' in kwargs:
|
|
instance.tenant = kwargs['tenant']
|
|
if 'tenant' not in self.filters:
|
|
del kwargs['tenant']
|
|
ret = f(*args, **kwargs)
|
|
(instance.format, instance.tenant) = (format, tenant)
|
|
return ret
|
|
return wrapped_f
|
|
|
|
|
|
class Client(object):
|
|
|
|
"""A base client class - derived from Glance.BaseClient"""
|
|
|
|
#Metadata for deserializing xml
|
|
_serialization_metadata = {
|
|
"application/xml": {
|
|
"attributes": {
|
|
"network": ["id", "name"],
|
|
"port": ["id", "state"],
|
|
"attachment": ["id"], },
|
|
"plurals": {
|
|
"networks": "network",
|
|
"ports": "port", }, }, }
|
|
|
|
# Action query strings
|
|
networks_path = "/networks"
|
|
network_path = "/networks/%s"
|
|
ports_path = "/networks/%s/ports"
|
|
port_path = "/networks/%s/ports/%s"
|
|
attachment_path = "/networks/%s/ports/%s/attachment"
|
|
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}",
|
|
retries=0,
|
|
retry_interval=1):
|
|
"""
|
|
Creates a new client to some service.
|
|
|
|
:param host: The host where service resides
|
|
:param port: The port where service resides
|
|
:param use_ssl: True to use SSL, False to use HTTP
|
|
:param tenant: The tenant ID to make requests with
|
|
:param format: The format to query the server with
|
|
:param testingStub: A class that stubs basic server methods for tests
|
|
:param key_file: The SSL key file to use if use_ssl is true
|
|
:param cert_file: The SSL cert file to use if use_ssl is true
|
|
: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
|
|
self.use_ssl = use_ssl
|
|
self.tenant = tenant
|
|
self.format = format
|
|
self.connection = None
|
|
self.testingStub = testingStub
|
|
self.key_file = key_file
|
|
self.cert_file = cert_file
|
|
self.logger = logger
|
|
if not self.logger:
|
|
self.logger = LOG
|
|
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
|
|
error_message = response_body
|
|
LOG.debug("Server returned error: %s", status_code)
|
|
LOG.debug("Error message: %s", error_message)
|
|
# Add deserialized error message to exception arguments
|
|
try:
|
|
des_error_body = Serializer().deserialize(error_message,
|
|
self.content_type())
|
|
except:
|
|
# If unable to deserialized body it is probably not a
|
|
# Quantum error
|
|
des_error_body = {'message': error_message}
|
|
# Raise the appropriate exception
|
|
EXCEPTION_HANDLERS[self.version](status_code, des_error_body)
|
|
|
|
def get_connection_type(self):
|
|
"""
|
|
Returns the proper connection type
|
|
"""
|
|
if self.testingStub:
|
|
return self.testingStub
|
|
if self.use_ssl:
|
|
return httplib.HTTPSConnection
|
|
else:
|
|
return httplib.HTTPConnection
|
|
|
|
def _send_request(self, conn, method, action, body, headers):
|
|
# Salvatore: Isolating this piece of code in its own method to
|
|
# facilitate stubout for testing
|
|
if self.logger:
|
|
self.logger.debug("Quantum Client Request:\n"
|
|
+ method + " " + action + "\n")
|
|
if body:
|
|
self.logger.debug(body)
|
|
conn.request(method, action, body, headers)
|
|
return conn.getresponse()
|
|
|
|
def do_request(self, method, action, body=None,
|
|
headers=None, params=None):
|
|
"""
|
|
Connects to the server and issues a request.
|
|
Returns the result data, or raises an appropriate exception if
|
|
HTTP status code is not 2xx
|
|
|
|
:param method: HTTP method ("GET", "POST", "PUT", etc...)
|
|
:param body: string of data to send, or None (default)
|
|
: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
|
|
if not self.tenant:
|
|
raise Exception("Tenant ID not set")
|
|
# Add format and tenant_id
|
|
action += ".%s" % self.format
|
|
action = self.action_prefix + action
|
|
action = action.replace('{tenant_id}', self.tenant)
|
|
|
|
if type(params) is dict:
|
|
action += '?' + urllib.urlencode(params)
|
|
if body:
|
|
body = self.serialize(body)
|
|
try:
|
|
connection_type = self.get_connection_type()
|
|
headers = headers or {"Content-Type":
|
|
"application/%s" % self.format}
|
|
# if available, add authentication token
|
|
if self.auth_token:
|
|
headers[AUTH_TOKEN_HEADER] = self.auth_token
|
|
# Open connection and send request, handling SSL certs
|
|
certs = {'key_file': self.key_file, 'cert_file': self.cert_file}
|
|
certs = dict((x, certs[x]) for x in certs if certs[x] is not None)
|
|
if self.use_ssl and len(certs):
|
|
conn = connection_type(self.host, self.port, **certs)
|
|
else:
|
|
conn = connection_type(self.host, self.port)
|
|
res = self._send_request(conn, method, action, body, headers)
|
|
status_code = self.get_status_code(res)
|
|
data = res.read()
|
|
utils.http_log(LOG, [method, action],
|
|
{'body': body,
|
|
'headers': headers,
|
|
}, res, data)
|
|
if self.logger:
|
|
self.logger.debug("Quantum Client Reply (code = %s) :\n %s" %
|
|
(str(status_code), data))
|
|
if status_code in (httplib.OK,
|
|
httplib.CREATED,
|
|
httplib.ACCEPTED,
|
|
httplib.NO_CONTENT):
|
|
return self.deserialize(data, status_code)
|
|
else:
|
|
self._handle_fault_response(status_code, data)
|
|
except (socket.error, IOError), e:
|
|
exc = exceptions.ConnectionFailed(reason=str(e))
|
|
LOG.exception(exc.message)
|
|
raise exc
|
|
|
|
def get_status_code(self, response):
|
|
"""
|
|
Returns the integer status code from the response, which
|
|
can be either a Webob.Response (used in testing) or httplib.Response
|
|
"""
|
|
if hasattr(response, 'status_int'):
|
|
return response.status_int
|
|
else:
|
|
return response.status
|
|
|
|
def serialize(self, data):
|
|
"""
|
|
Serializes a dictionary with a single key (which can contain any
|
|
structure) into either xml or json
|
|
"""
|
|
if data is None:
|
|
return None
|
|
elif type(data) is dict:
|
|
return Serializer().serialize(data, self.content_type())
|
|
else:
|
|
raise Exception("unable to serialize object of type = '%s'" %
|
|
type(data))
|
|
|
|
def deserialize(self, data, status_code):
|
|
"""
|
|
Deserializes a an xml or json string into a dictionary
|
|
"""
|
|
if status_code == 204:
|
|
return data
|
|
return Serializer(self._serialization_metadata).deserialize(
|
|
data, self.content_type())
|
|
|
|
def content_type(self, format=None):
|
|
"""
|
|
Returns the mime-type for either 'xml' or 'json'. Defaults to the
|
|
currently set format
|
|
"""
|
|
if not format:
|
|
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.get(self.networks_path)
|
|
|
|
@ApiCall
|
|
def list_networks_details(self):
|
|
"""
|
|
Fetches a detailed list of all networks for a tenant
|
|
"""
|
|
return self.get(self.networks_path + self.detail_path)
|
|
|
|
@ApiCall
|
|
def show_network(self, network):
|
|
"""
|
|
Fetches information of a certain network
|
|
"""
|
|
return self.get(self.network_path % (network))
|
|
|
|
@ApiCall
|
|
def show_network_details(self, network):
|
|
"""
|
|
Fetches the details of a certain network
|
|
"""
|
|
return self.get((self.network_path + self.detail_path) % (network))
|
|
|
|
@ApiCall
|
|
def create_network(self, body=None):
|
|
"""
|
|
Creates a new network
|
|
"""
|
|
return self.post(self.networks_path, body=body)
|
|
|
|
@ApiCall
|
|
def update_network(self, network, body=None):
|
|
"""
|
|
Updates a network
|
|
"""
|
|
return self.put(self.network_path % (network), body=body)
|
|
|
|
@ApiCall
|
|
def delete_network(self, network):
|
|
"""
|
|
Deletes the specified 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.get(self.ports_path % (network))
|
|
|
|
@ApiCall
|
|
def list_ports_details(self, network):
|
|
"""
|
|
Fetches a detailed list of ports on a given 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.get(self.port_path % (network, port))
|
|
|
|
@ApiCall
|
|
def show_port_details(self, network, port):
|
|
"""
|
|
Fetches the details of a certain 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.post(self.ports_path % (network), body=body)
|
|
|
|
@ApiCall
|
|
def delete_port(self, network, port):
|
|
"""
|
|
Deletes the specified port from a network
|
|
"""
|
|
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.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.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.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.delete(self.attachment_path % (network, port))
|
|
|
|
|
|
class ClientV11(Client):
|
|
"""
|
|
This class overiddes some methods of the Client class in order to deal with
|
|
features specific to API v1.1 such as filters
|
|
"""
|
|
|
|
@APIFilterCall(net_filters_v11)
|
|
def list_networks(self, **filters):
|
|
"""
|
|
Fetches a list of all networks for a tenant
|
|
"""
|
|
# Pass filters in "params" argument to do_request
|
|
return self.get(self.networks_path, params=filters)
|
|
|
|
@APIFilterCall(net_filters_v11)
|
|
def list_networks_details(self, **filters):
|
|
"""
|
|
Fetches a detailed list of all networks for a tenant
|
|
"""
|
|
return self.get(self.networks_path + self.detail_path, params=filters)
|
|
|
|
@APIFilterCall(port_filters_v11)
|
|
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.get(self.ports_path % (network), params=filters)
|
|
|
|
@APIFilterCall(port_filters_v11)
|
|
def list_ports_details(self, network, **filters):
|
|
"""
|
|
Fetches a detailed list of ports on a given network
|
|
"""
|
|
return self.get((self.ports_path + self.detail_path) % (network),
|
|
params=filters)
|