1 # Copyright 2010 Jacob Kaplan-Moss
2 # Copyright 2011 OpenStack Foundation
3 # Copyright 2011 Piston Cloud Computing, Inc.
4 # Copyright 2013 Alessio Ababilov
5 # Copyright 2013 Grid Dynamics
6 # Copyright 2013 OpenStack Foundation
9 # Licensed under the Apache License, Version 2.0 (the "License"); you may
10 # not use this file except in compliance with the License. You may obtain
11 # a copy of the License at
13 # http://www.apache.org/licenses/LICENSE-2.0
15 # Unless required by applicable law or agreed to in writing, software
16 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
17 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
18 # License for the specific language governing permissions and limitations
22 OpenStack Client interface. Handles the REST calls and responses.
25 # E0202: An attribute inherited from %s hide this method
26 # pylint: disable=E0202
33 import simplejson as json
37 from oslo.utils import encodeutils
38 from oslo.utils import importutils
41 from escalatorclient.openstack.common._i18n import _
42 from escalatorclient.openstack.common.apiclient import exceptions
44 _logger = logging.getLogger(__name__)
45 SENSITIVE_HEADERS = ('X-Auth-Token', 'X-Subject-Token',)
48 class HTTPClient(object):
49 """This client handles sending HTTP requests to OpenStack servers.
53 - share authentication information between several clients to different
54 services (e.g., for compute and image clients);
55 - reissue authentication request for expired tokens;
56 - encode/decode JSON bodies;
57 - raise exceptions on HTTP errors;
58 - pluggable authentication;
59 - store authentication information in a keyring;
60 - store time spent for requests;
61 - register clients for particular services, so one can use
62 `http_client.identity` or `http_client.compute`;
63 - log requests and responses in a format that is easy to copy-and-paste
64 into terminal and send the same request with curl.
67 user_agent = "escalatorclient.openstack.common.apiclient"
72 endpoint_type="publicURL",
82 self.auth_plugin = auth_plugin
84 self.endpoint_type = endpoint_type
85 self.region_name = region_name
87 self.original_ip = original_ip
88 self.timeout = timeout
92 self.keyring_saver = keyring_saver
94 self.user_agent = user_agent or self.user_agent
96 self.times = [] # [("item", starttime, endtime), ...]
97 self.timings = timings
99 # requests within the same session can reuse TCP connections from pool
100 self.http = http or requests.Session()
102 self.cached_token = None
103 self.last_request_id = None
105 def _safe_header(self, name, value):
106 if name in SENSITIVE_HEADERS:
107 # because in python3 byte string handling is ... ug
108 v = value.encode('utf-8')
111 return encodeutils.safe_decode(name), "{SHA1}%s" % d
113 return (encodeutils.safe_decode(name),
114 encodeutils.safe_decode(value))
116 def _http_log_req(self, method, url, kwargs):
126 for element in kwargs['headers']:
127 header = ("-H '%s: %s'" %
128 self._safe_header(element, kwargs['headers'][element]))
129 string_parts.append(header)
131 _logger.debug("REQ: %s" % " ".join(string_parts))
133 _logger.debug("REQ BODY: %s\n" % (kwargs['data']))
135 def _http_log_resp(self, resp):
142 if resp._content_consumed:
147 def serialize(self, kwargs):
148 if kwargs.get('json') is not None:
149 kwargs['headers']['Content-Type'] = 'application/json'
150 kwargs['data'] = json.dumps(kwargs['json'])
156 def get_timings(self):
159 def reset_timings(self):
162 def request(self, method, url, **kwargs):
163 """Send an http request with the specified characteristics.
165 Wrapper around `requests.Session.request` to handle tasks such as
166 setting headers, JSON encoding/decoding, and error handling.
168 :param method: method of HTTP request
169 :param url: URL of HTTP request
170 :param kwargs: any other parameter that can be passed to
171 requests.Session.request (such as `headers`) or `json`
172 that will be encoded as JSON and used as `data` argument
174 kwargs.setdefault("headers", {})
175 kwargs["headers"]["User-Agent"] = self.user_agent
177 kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
178 self.original_ip, self.user_agent)
179 if self.timeout is not None:
180 kwargs.setdefault("timeout", self.timeout)
181 kwargs.setdefault("verify", self.verify)
182 if self.cert is not None:
183 kwargs.setdefault("cert", self.cert)
184 self.serialize(kwargs)
186 self._http_log_req(method, url, kwargs)
188 start_time = time.time()
189 resp = self.http.request(method, url, **kwargs)
191 self.times.append(("%s %s" % (method, url),
192 start_time, time.time()))
193 self._http_log_resp(resp)
195 self.last_request_id = resp.headers.get('x-openstack-request-id')
197 if resp.status_code >= 400:
199 "Request returned failure status: %s",
201 raise exceptions.from_response(resp, method, url)
206 def concat_url(endpoint, url):
207 """Concatenate endpoint and final URL.
209 E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to
210 "http://keystone/v2.0/tokens".
212 :param endpoint: the base URL
213 :param url: the final URL
215 return "%s/%s" % (endpoint.rstrip("/"), url.strip("/"))
217 def client_request(self, client, method, url, **kwargs):
218 """Send an http request using `client`'s endpoint and specified `url`.
220 If request was rejected as unauthorized (possibly because the token is
221 expired), issue one authorization attempt and send the request once
224 :param client: instance of BaseClient descendant
225 :param method: method of HTTP request
226 :param url: URL of HTTP request
227 :param kwargs: any other parameter that can be passed to
232 "endpoint_type": client.endpoint_type or self.endpoint_type,
233 "service_type": client.service_type,
235 token, endpoint = (self.cached_token, client.cached_endpoint)
236 just_authenticated = False
237 if not (token and endpoint):
239 token, endpoint = self.auth_plugin.token_and_endpoint(
241 except exceptions.EndpointException:
243 if not (token and endpoint):
245 just_authenticated = True
246 token, endpoint = self.auth_plugin.token_and_endpoint(
248 if not (token and endpoint):
249 raise exceptions.AuthorizationFailure(
250 _("Cannot find endpoint or token for request"))
252 old_token_endpoint = (token, endpoint)
253 kwargs.setdefault("headers", {})["X-Auth-Token"] = token
254 self.cached_token = token
255 client.cached_endpoint = endpoint
256 # Perform the request once. If we get Unauthorized, then it
257 # might be because the auth token expired, so try to
258 # re-authenticate and try again. If it still fails, bail.
261 method, self.concat_url(endpoint, url), **kwargs)
262 except exceptions.Unauthorized as unauth_ex:
263 if just_authenticated:
265 self.cached_token = None
266 client.cached_endpoint = None
267 if self.auth_plugin.opts.get('token'):
268 self.auth_plugin.opts['token'] = None
269 if self.auth_plugin.opts.get('endpoint'):
270 self.auth_plugin.opts['endpoint'] = None
273 token, endpoint = self.auth_plugin.token_and_endpoint(
275 except exceptions.EndpointException:
277 if (not (token and endpoint) or
278 old_token_endpoint == (token, endpoint)):
280 self.cached_token = token
281 client.cached_endpoint = endpoint
282 kwargs["headers"]["X-Auth-Token"] = token
284 method, self.concat_url(endpoint, url), **kwargs)
286 def add_client(self, base_client_instance):
287 """Add a new instance of :class:`BaseClient` descendant.
289 `self` will store a reference to `base_client_instance`.
293 >>> def test_clients():
294 ... from keystoneclient.auth import keystone
295 ... from openstack.common.apiclient import client
296 ... auth = keystone.KeystoneAuthPlugin(
297 ... username="user", password="pass", tenant_name="tenant",
298 ... auth_url="http://auth:5000/v2.0")
299 ... openstack_client = client.HTTPClient(auth)
300 ... # create nova client
301 ... from novaclient.v1_1 import client
302 ... client.Client(openstack_client)
303 ... # create keystone client
304 ... from keystoneclient.v2_0 import client
305 ... client.Client(openstack_client)
307 ... openstack_client.identity.tenants.list()
308 ... openstack_client.compute.servers.list()
310 service_type = base_client_instance.service_type
311 if service_type and not hasattr(self, service_type):
312 setattr(self, service_type, base_client_instance)
314 def authenticate(self):
315 self.auth_plugin.authenticate(self)
316 # Store the authentication results in the keyring for later requests
317 if self.keyring_saver:
318 self.keyring_saver.save(self)
321 class BaseClient(object):
322 """Top-level object to access the OpenStack API.
324 This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient`
325 will handle a bunch of issues such as authentication.
329 endpoint_type = None # "publicURL" will be used
330 cached_endpoint = None
332 def __init__(self, http_client, extensions=None):
333 self.http_client = http_client
334 http_client.add_client(self)
336 # Add in any extensions...
338 for extension in extensions:
339 if extension.manager_class:
340 setattr(self, extension.name,
341 extension.manager_class(self))
343 def client_request(self, method, url, **kwargs):
344 return self.http_client.client_request(
345 self, method, url, **kwargs)
348 def last_request_id(self):
349 return self.http_client.last_request_id
351 def head(self, url, **kwargs):
352 return self.client_request("HEAD", url, **kwargs)
354 def get(self, url, **kwargs):
355 return self.client_request("GET", url, **kwargs)
357 def post(self, url, **kwargs):
358 return self.client_request("POST", url, **kwargs)
360 def put(self, url, **kwargs):
361 return self.client_request("PUT", url, **kwargs)
363 def delete(self, url, **kwargs):
364 return self.client_request("DELETE", url, **kwargs)
366 def patch(self, url, **kwargs):
367 return self.client_request("PATCH", url, **kwargs)
370 def get_class(api_name, version, version_map):
371 """Returns the client class for the requested API version
373 :param api_name: the name of the API, e.g. 'compute', 'image', etc
374 :param version: the requested API version
375 :param version_map: a dict of client classes keyed by version
376 :rtype: a client class for the requested API version
379 client_path = version_map[str(version)]
380 except (KeyError, ValueError):
381 msg = _("Invalid %(api_name)s client version '%(version)s'. "
382 "Must be one of: %(version_map)s") % {
383 'api_name': api_name,
385 'version_map': ', '.join(version_map.keys())}
386 raise exceptions.UnsupportedVersion(msg)
388 return importutils.import_class(client_path)