bf7a0e2ce7
When NVP returns a 405 error code no exception is raised, causing the plugin to assume that everything went well when it didn't. Fixes bug #1243411 Change-Id: If70db05b8c6950bfa88a36c4f0c20ee80419d31d
261 lines
9.2 KiB
Python
261 lines
9.2 KiB
Python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
# Copyright 2012 Nicira, Inc.
|
|
# 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: Somik Behera, Nicira Networks, Inc.
|
|
|
|
import httplib # basic HTTP library for HTTPS connections
|
|
import logging
|
|
from neutron.plugins.nicira.api_client import (
|
|
client_eventlet, request_eventlet)
|
|
|
|
LOG = logging.getLogger("NVPApiHelper")
|
|
LOG.setLevel(logging.INFO)
|
|
|
|
|
|
def _find_nvp_version_in_headers(headers):
|
|
# be safe if headers is None - do not cause a failure
|
|
for (header_name, header_value) in (headers or ()):
|
|
try:
|
|
if header_name == 'server':
|
|
return NVPVersion(header_value.split('/')[1])
|
|
except IndexError:
|
|
LOG.warning(_("Unable to fetch NVP version from response "
|
|
"headers:%s"), headers)
|
|
|
|
|
|
class NVPVersion(object):
|
|
"""Abstracts NVP version by exposing major and minor."""
|
|
|
|
def __init__(self, nvp_version):
|
|
self.full_version = nvp_version.split('.')
|
|
self.major = int(self.full_version[0])
|
|
self.minor = int(self.full_version[1])
|
|
|
|
def __str__(self):
|
|
return '.'.join(self.full_version)
|
|
|
|
|
|
class NVPApiHelper(client_eventlet.NvpApiClientEventlet):
|
|
'''API helper class.
|
|
|
|
Helper class to do basic login, cookie management, and provide base
|
|
method to send HTTP requests.
|
|
|
|
Implements new eventlet-based framework derived from the management
|
|
console nvp_gevent_client module.
|
|
'''
|
|
|
|
def __init__(self, api_providers, user, password, request_timeout,
|
|
http_timeout, retries, redirects,
|
|
concurrent_connections=10, nvp_gen_timeout=-1):
|
|
'''Constructor.
|
|
|
|
:param api_providers: a list of tuples in the form:
|
|
(host, port, is_ssl=True). Passed on to NvpClientEventlet.
|
|
:param user: the login username.
|
|
:param password: the login password.
|
|
:param concurrent_connections: the number of concurrent connections.
|
|
:param request_timeout: all operations (including retries, redirects
|
|
from unresponsive controllers, etc) should finish within this
|
|
timeout.
|
|
:param http_timeout: how long to wait before aborting an
|
|
unresponsive controller (and allow for retries to another
|
|
controller in the cluster)
|
|
:param retries: the number of concurrent connections.
|
|
:param redirects: the number of concurrent connections.
|
|
'''
|
|
client_eventlet.NvpApiClientEventlet.__init__(
|
|
self, api_providers, user, password, concurrent_connections,
|
|
nvp_gen_timeout)
|
|
|
|
self._request_timeout = request_timeout
|
|
self._http_timeout = http_timeout
|
|
self._retries = retries
|
|
self._redirects = redirects
|
|
self._nvp_version = None
|
|
|
|
# NOTE(salvatore-orlando): This method is not used anymore. Login is now
|
|
# performed automatically inside the request eventlet if necessary.
|
|
def login(self, user=None, password=None):
|
|
'''Login to NVP controller.
|
|
|
|
Assumes same password is used for all controllers.
|
|
|
|
:param user: NVP controller user (usually admin). Provided for
|
|
backwards compatability. In the normal mode of operation
|
|
this should be None.
|
|
:param password: NVP controller password. Provided for backwards
|
|
compatability. In the normal mode of operation this should
|
|
be None.
|
|
|
|
:returns: Does not return a value.
|
|
'''
|
|
if user:
|
|
self._user = user
|
|
if password:
|
|
self._password = password
|
|
|
|
return client_eventlet.NvpApiClientEventlet._login(self)
|
|
|
|
def request(self, method, url, body="", content_type="application/json"):
|
|
'''Issues request to controller.'''
|
|
|
|
g = request_eventlet.NvpGenericRequestEventlet(
|
|
self, method, url, body, content_type, auto_login=True,
|
|
request_timeout=self._request_timeout,
|
|
http_timeout=self._http_timeout,
|
|
retries=self._retries, redirects=self._redirects)
|
|
g.start()
|
|
response = g.join()
|
|
LOG.debug(_('NVPApiHelper.request() returns "%s"'), response)
|
|
|
|
# response is a modified HTTPResponse object or None.
|
|
# response.read() will not work on response as the underlying library
|
|
# request_eventlet.NvpApiRequestEventlet has already called this
|
|
# method in order to extract the body and headers for processing.
|
|
# NvpApiRequestEventlet derived classes call .read() and
|
|
# .getheaders() on the HTTPResponse objects and store the results in
|
|
# the response object's .body and .headers data members for future
|
|
# access.
|
|
|
|
if response is None:
|
|
# Timeout.
|
|
LOG.error(_('Request timed out: %(method)s to %(url)s'),
|
|
{'method': method, 'url': url})
|
|
raise RequestTimeout()
|
|
|
|
status = response.status
|
|
if status == httplib.UNAUTHORIZED:
|
|
raise UnAuthorizedRequest()
|
|
|
|
# Fail-fast: Check for exception conditions and raise the
|
|
# appropriate exceptions for known error codes.
|
|
if status in self.error_codes:
|
|
LOG.error(_("Received error code: %s"), status)
|
|
LOG.error(_("Server Error Message: %s"), response.body)
|
|
self.error_codes[status](self, response)
|
|
|
|
# Continue processing for non-error condition.
|
|
if (status != httplib.OK and status != httplib.CREATED
|
|
and status != httplib.NO_CONTENT):
|
|
LOG.error(_("%(method)s to %(url)s, unexpected response code: "
|
|
"%(status)d (content = '%(body)s')"),
|
|
{'method': method, 'url': url,
|
|
'status': response.status, 'body': response.body})
|
|
return None
|
|
|
|
if not self._nvp_version:
|
|
self._nvp_version = _find_nvp_version_in_headers(response.headers)
|
|
|
|
return response.body
|
|
|
|
def get_nvp_version(self):
|
|
if not self._nvp_version:
|
|
# Determine the NVP version by querying the control
|
|
# cluster nodes. Currently, the version will be the
|
|
# one of the server that responds.
|
|
self.request('GET', '/ws.v1/control-cluster/node')
|
|
if not self._nvp_version:
|
|
LOG.error(_('Unable to determine NVP version. '
|
|
'Plugin might not work as expected.'))
|
|
return self._nvp_version
|
|
|
|
def fourZeroFour(self, response=None):
|
|
raise ResourceNotFound()
|
|
|
|
def fourZeroNine(self, response=None):
|
|
raise Conflict()
|
|
|
|
def fiveZeroThree(self, response=None):
|
|
raise ServiceUnavailable()
|
|
|
|
def fourZeroThree(self, response=None):
|
|
if 'read-only' in response.body:
|
|
raise ReadOnlyMode()
|
|
else:
|
|
raise Forbidden()
|
|
|
|
def zero(self, response=None):
|
|
raise NvpApiException()
|
|
|
|
# TODO(del): ensure error_codes are handled/raised appropriately
|
|
# in api_client.
|
|
error_codes = {404: fourZeroFour,
|
|
405: zero,
|
|
409: fourZeroNine,
|
|
503: fiveZeroThree,
|
|
403: fourZeroThree,
|
|
301: zero,
|
|
307: zero,
|
|
400: zero,
|
|
500: zero,
|
|
501: zero,
|
|
503: zero}
|
|
|
|
|
|
class NvpApiException(Exception):
|
|
"""Base NvpApiClient Exception.
|
|
|
|
To correctly use this class, inherit from it and define
|
|
a 'message' property. That message will get printf'd
|
|
with the keyword arguments provided to the constructor.
|
|
|
|
"""
|
|
message = _("An unknown exception occurred.")
|
|
|
|
def __init__(self, **kwargs):
|
|
try:
|
|
self._error_string = self.message % kwargs
|
|
|
|
except Exception:
|
|
# at least get the core message out if something happened
|
|
self._error_string = self.message
|
|
|
|
def __str__(self):
|
|
return self._error_string
|
|
|
|
|
|
class UnAuthorizedRequest(NvpApiException):
|
|
message = _("Server denied session's authentication credentials.")
|
|
|
|
|
|
class ResourceNotFound(NvpApiException):
|
|
message = _("An entity referenced in the request was not found.")
|
|
|
|
|
|
class Conflict(NvpApiException):
|
|
message = _("Request conflicts with configuration on a different "
|
|
"entity.")
|
|
|
|
|
|
class ServiceUnavailable(NvpApiException):
|
|
message = _("Request could not completed because the associated "
|
|
"resource could not be reached.")
|
|
|
|
|
|
class Forbidden(NvpApiException):
|
|
message = _("The request is forbidden from accessing the "
|
|
"referenced resource.")
|
|
|
|
|
|
class ReadOnlyMode(Forbidden):
|
|
message = _("Create/Update actions are forbidden when in read-only mode.")
|
|
|
|
|
|
class RequestTimeout(NvpApiException):
|
|
message = _("The request has timed out.")
|