# Copyright 2012 VMware, 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. # import abc import copy from http import client as httplib import socket import time import urllib import eventlet from oslo_log import log as logging from oslo_utils import excutils from vmware_nsx._i18n import _ from vmware_nsx import api_client LOG = logging.getLogger(__name__) DEFAULT_HTTP_TIMEOUT = 30 DEFAULT_RETRIES = 2 DEFAULT_REDIRECTS = 2 DEFAULT_API_REQUEST_POOL_SIZE = 1000 DEFAULT_MAXIMUM_REQUEST_ID = 4294967295 DOWNLOAD_TIMEOUT = 180 class ApiRequest(object, metaclass=abc.ABCMeta): '''An abstract baseclass for all ApiRequest implementations. This defines the interface and property structure for both eventlet and gevent-based ApiRequest classes. ''' # List of allowed status codes. ALLOWED_STATUS_CODES = [ httplib.OK, httplib.CREATED, httplib.NO_CONTENT, httplib.MOVED_PERMANENTLY, httplib.TEMPORARY_REDIRECT, httplib.BAD_REQUEST, httplib.UNAUTHORIZED, httplib.FORBIDDEN, httplib.NOT_FOUND, httplib.CONFLICT, httplib.INTERNAL_SERVER_ERROR, httplib.SERVICE_UNAVAILABLE ] @abc.abstractmethod def start(self): pass @abc.abstractmethod def join(self): pass @abc.abstractmethod def copy(self): pass def _issue_request(self): '''Issue a request to a provider.''' conn = (self._client_conn or self._api_client.acquire_connection(True, copy.copy(self._headers), rid=self._rid())) if conn is None: error = Exception(_("No API connections available")) self._request_error = error return error url = self._url LOG.debug("[%(rid)d] Issuing - request url: %(conn)s " "body: %(body)s", {'rid': self._rid(), 'conn': self._request_str(conn, url), 'body': self._body}) issued_time = time.time() is_conn_error = False is_conn_service_unavail = False response = None try: redirects = 0 while (redirects <= self._redirects): # Update connection with user specified request timeout, # the connect timeout is usually smaller so we only set # the request timeout after a connection is established if conn.sock is None: conn.connect() conn.sock.settimeout(self._http_timeout) elif conn.sock.gettimeout() != self._http_timeout: conn.sock.settimeout(self._http_timeout) headers = copy.copy(self._headers) cookie = self._api_client.auth_cookie(conn) if cookie: headers["Cookie"] = cookie gen = self._api_client.config_gen if gen: headers["X-Nvp-Wait-For-Config-Generation"] = gen LOG.debug("Setting X-Nvp-Wait-For-Config-Generation " "request header: '%s'", gen) try: conn.request(self._method, url, self._body, headers) except Exception as e: with excutils.save_and_reraise_exception(): LOG.warning("[%(rid)d] Exception issuing request: " "%(e)s", {'rid': self._rid(), 'e': e}) response = conn.getresponse() response.body = response.read() response.headers = response.getheaders() elapsed_time = time.time() - issued_time LOG.debug("[%(rid)d] Completed request '%(conn)s': " "%(status)s (%(elapsed)s seconds)", {'rid': self._rid(), 'conn': self._request_str(conn, url), 'status': response.status, 'elapsed': elapsed_time}) new_gen = response.getheader('X-Nvp-Config-Generation', None) if new_gen: LOG.debug("Reading X-Nvp-config-Generation response " "header: '%s'", new_gen) if (self._api_client.config_gen is None or self._api_client.config_gen < int(new_gen)): self._api_client.config_gen = int(new_gen) if response.status == httplib.UNAUTHORIZED: # If request is unauthorized, clear the session cookie # for the current provider so that subsequent requests # to the same provider triggers re-authentication. self._api_client.set_auth_cookie(conn, None) elif response.status == httplib.SERVICE_UNAVAILABLE: is_conn_service_unavail = True if response.status not in [httplib.MOVED_PERMANENTLY, httplib.TEMPORARY_REDIRECT]: break if redirects >= self._redirects: LOG.info("[%d] Maximum redirects exceeded, aborting " "request", self._rid()) break redirects += 1 conn, url = self._redirect_params(conn, response.headers, self._client_conn is None) if url is None: response.status = httplib.INTERNAL_SERVER_ERROR break LOG.info("[%(rid)d] Redirecting request to: %(conn)s", {'rid': self._rid(), 'conn': self._request_str(conn, url)}) # yield here, just in case we are not out of the loop yet eventlet.greenthread.sleep(0) # If we receive any of these responses, then # our server did not process our request and may be in an # errored state. Raise an exception, which will cause the # conn to be released with is_conn_error == True # which puts the conn on the back of the client's priority # queue. if (response.status == httplib.INTERNAL_SERVER_ERROR and response.status > httplib.NOT_IMPLEMENTED): LOG.warning("[%(rid)d] Request '%(method)s %(url)s' " "received: %(status)s", {'rid': self._rid(), 'method': self._method, 'url': self._url, 'status': response.status}) raise Exception(_('Server error return: %s'), response.status) return response except socket.error: is_conn_service_unavail = True except Exception as e: if isinstance(e, httplib.BadStatusLine): msg = (_("Invalid server response")) else: msg = str(e) if response is None: elapsed_time = time.time() - issued_time LOG.warning("[%(rid)d] Failed request '%(conn)s': '%(msg)s' " "(%(elapsed)s seconds)", {'rid': self._rid(), 'conn': self._request_str(conn, url), 'msg': msg, 'elapsed': elapsed_time}) self._request_error = e is_conn_error = True return e finally: # Make sure we release the original connection provided by the # acquire_connection() call above. if self._client_conn is None: self._api_client.release_connection(conn, is_conn_error, is_conn_service_unavail, rid=self._rid()) def _redirect_params(self, conn, headers, allow_release_conn=False): """Process redirect response, create new connection if necessary. Args: conn: connection that returned the redirect response headers: response headers of the redirect response allow_release_conn: if redirecting to a different server, release existing connection back to connection pool. Returns: Return tuple(conn, url) where conn is a connection object to the redirect target and url is the path of the API request """ url = None for name, value in headers: if name.lower() == "location": url = value break if not url: LOG.warning("[%d] Received redirect status without location " "header field", self._rid()) return (conn, None) # Accept location with the following format: # 1. /path, redirect to same node # 2. scheme://hostname:[port]/path where scheme is https or http # Reject others # 3. e.g. relative paths, unsupported scheme, unspecified host result = urllib.parse.urlparse(url) if not result.scheme and not result.hostname and result.path: if result.path[0] == "/": if result.query: url = "%s?%s" % (result.path, result.query) else: url = result.path return (conn, url) # case 1 LOG.warning("[%(rid)d] Received invalid redirect " "location: '%(url)s'", {'rid': self._rid(), 'url': url}) return (conn, None) # case 3 if result.scheme not in ["http", "https"] or not result.hostname: LOG.warning("[%(rid)d] Received malformed redirect " "location: %(url)s", {'rid': self._rid(), 'url': url}) return (conn, None) # case 3 # case 2, redirect location includes a scheme # so setup a new connection and authenticate if allow_release_conn: self._api_client.release_connection(conn) conn_params = (result.hostname, result.port, result.scheme == "https") conn = self._api_client.acquire_redirect_connection(conn_params, True, self._headers) if result.query: url = "%s?%s" % (result.path, result.query) else: url = result.path return (conn, url) def _rid(self): '''Return current request id.''' return self._request_id @property def request_error(self): '''Return any errors associated with this instance.''' return self._request_error def _request_str(self, conn, url): '''Return string representation of connection.''' return "%s %s/%s" % (self._method, api_client.ctrl_conn_to_str(conn), url)