add escalator cli framework
[escalator.git] / client / escalatorclient / openstack / common / apiclient / client.py
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
7 # All Rights Reserved.
8 #
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
12 #
13 #         http://www.apache.org/licenses/LICENSE-2.0
14 #
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
19 #    under the License.
20
21 """
22 OpenStack Client interface. Handles the REST calls and responses.
23 """
24
25 # E0202: An attribute inherited from %s hide this method
26 # pylint: disable=E0202
27
28 import hashlib
29 import logging
30 import time
31
32 try:
33     import simplejson as json
34 except ImportError:
35     import json
36
37 from oslo_utils import encodeutils
38 from oslo_utils import importutils
39 import requests
40
41 from escalatorclient.openstack.common._i18n import _
42 from escalatorclient.openstack.common.apiclient import exceptions
43
44 _logger = logging.getLogger(__name__)
45 SENSITIVE_HEADERS = ('X-Auth-Token', 'X-Subject-Token',)
46
47
48 class HTTPClient(object):
49     """This client handles sending HTTP requests to OpenStack servers.
50
51     Features:
52
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.
65     """
66
67     user_agent = "escalatorclient.openstack.common.apiclient"
68
69     def __init__(self,
70                  auth_plugin,
71                  region_name=None,
72                  endpoint_type="publicURL",
73                  original_ip=None,
74                  verify=True,
75                  cert=None,
76                  timeout=None,
77                  timings=False,
78                  keyring_saver=None,
79                  debug=False,
80                  user_agent=None,
81                  http=None):
82         self.auth_plugin = auth_plugin
83
84         self.endpoint_type = endpoint_type
85         self.region_name = region_name
86
87         self.original_ip = original_ip
88         self.timeout = timeout
89         self.verify = verify
90         self.cert = cert
91
92         self.keyring_saver = keyring_saver
93         self.debug = debug
94         self.user_agent = user_agent or self.user_agent
95
96         self.times = []  # [("item", starttime, endtime), ...]
97         self.timings = timings
98
99         # requests within the same session can reuse TCP connections from pool
100         self.http = http or requests.Session()
101
102         self.cached_token = None
103         self.last_request_id = None
104
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')
109             h = hashlib.sha1(v)
110             d = h.hexdigest()
111             return encodeutils.safe_decode(name), "{SHA1}%s" % d
112         else:
113             return (encodeutils.safe_decode(name),
114                     encodeutils.safe_decode(value))
115
116     def _http_log_req(self, method, url, kwargs):
117         if not self.debug:
118             return
119
120         string_parts = [
121             "curl -g -i",
122             "-X '%s'" % method,
123             "'%s'" % url,
124         ]
125
126         for element in kwargs['headers']:
127             header = ("-H '%s: %s'" %
128                       self._safe_header(element, kwargs['headers'][element]))
129             string_parts.append(header)
130
131         _logger.debug("REQ: %s" % " ".join(string_parts))
132         if 'data' in kwargs:
133             _logger.debug("REQ BODY: %s\n" % (kwargs['data']))
134
135     def _http_log_resp(self, resp):
136         if not self.debug:
137             return
138         _logger.debug(
139             "RESP: [%s] %s\n",
140             resp.status_code,
141             resp.headers)
142         if resp._content_consumed:
143             _logger.debug(
144                 "RESP BODY: %s\n",
145                 resp.text)
146
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'])
151         try:
152             del kwargs['json']
153         except KeyError:
154             pass
155
156     def get_timings(self):
157         return self.times
158
159     def reset_timings(self):
160         self.times = []
161
162     def request(self, method, url, **kwargs):
163         """Send an http request with the specified characteristics.
164
165         Wrapper around `requests.Session.request` to handle tasks such as
166         setting headers, JSON encoding/decoding, and error handling.
167
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
173         """
174         kwargs.setdefault("headers", {})
175         kwargs["headers"]["User-Agent"] = self.user_agent
176         if self.original_ip:
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)
185
186         self._http_log_req(method, url, kwargs)
187         if self.timings:
188             start_time = time.time()
189         resp = self.http.request(method, url, **kwargs)
190         if self.timings:
191             self.times.append(("%s %s" % (method, url),
192                                start_time, time.time()))
193         self._http_log_resp(resp)
194
195         self.last_request_id = resp.headers.get('x-openstack-request-id')
196
197         if resp.status_code >= 400:
198             _logger.debug(
199                 "Request returned failure status: %s",
200                 resp.status_code)
201             raise exceptions.from_response(resp, method, url)
202
203         return resp
204
205     @staticmethod
206     def concat_url(endpoint, url):
207         """Concatenate endpoint and final URL.
208
209         E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to
210         "http://keystone/v2.0/tokens".
211
212         :param endpoint: the base URL
213         :param url: the final URL
214         """
215         return "%s/%s" % (endpoint.rstrip("/"), url.strip("/"))
216
217     def client_request(self, client, method, url, **kwargs):
218         """Send an http request using `client`'s endpoint and specified `url`.
219
220         If request was rejected as unauthorized (possibly because the token is
221         expired), issue one authorization attempt and send the request once
222         again.
223
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
228             `HTTPClient.request`
229         """
230
231         filter_args = {
232             "endpoint_type": client.endpoint_type or self.endpoint_type,
233             "service_type": client.service_type,
234         }
235         token, endpoint = (self.cached_token, client.cached_endpoint)
236         just_authenticated = False
237         if not (token and endpoint):
238             try:
239                 token, endpoint = self.auth_plugin.token_and_endpoint(
240                     **filter_args)
241             except exceptions.EndpointException:
242                 pass
243             if not (token and endpoint):
244                 self.authenticate()
245                 just_authenticated = True
246                 token, endpoint = self.auth_plugin.token_and_endpoint(
247                     **filter_args)
248                 if not (token and endpoint):
249                     raise exceptions.AuthorizationFailure(
250                         _("Cannot find endpoint or token for request"))
251
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.
259         try:
260             return self.request(
261                 method, self.concat_url(endpoint, url), **kwargs)
262         except exceptions.Unauthorized as unauth_ex:
263             if just_authenticated:
264                 raise
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
271             self.authenticate()
272             try:
273                 token, endpoint = self.auth_plugin.token_and_endpoint(
274                     **filter_args)
275             except exceptions.EndpointException:
276                 raise unauth_ex
277             if (not (token and endpoint) or
278                     old_token_endpoint == (token, endpoint)):
279                 raise unauth_ex
280             self.cached_token = token
281             client.cached_endpoint = endpoint
282             kwargs["headers"]["X-Auth-Token"] = token
283             return self.request(
284                 method, self.concat_url(endpoint, url), **kwargs)
285
286     def add_client(self, base_client_instance):
287         """Add a new instance of :class:`BaseClient` descendant.
288
289         `self` will store a reference to `base_client_instance`.
290
291         Example:
292
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)
306         ...     # use them
307         ...     openstack_client.identity.tenants.list()
308         ...     openstack_client.compute.servers.list()
309         """
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)
313
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)
319
320
321 class BaseClient(object):
322     """Top-level object to access the OpenStack API.
323
324     This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient`
325     will handle a bunch of issues such as authentication.
326     """
327
328     service_type = None
329     endpoint_type = None  # "publicURL" will be used
330     cached_endpoint = None
331
332     def __init__(self, http_client, extensions=None):
333         self.http_client = http_client
334         http_client.add_client(self)
335
336         # Add in any extensions...
337         if extensions:
338             for extension in extensions:
339                 if extension.manager_class:
340                     setattr(self, extension.name,
341                             extension.manager_class(self))
342
343     def client_request(self, method, url, **kwargs):
344         return self.http_client.client_request(
345             self, method, url, **kwargs)
346
347     @property
348     def last_request_id(self):
349         return self.http_client.last_request_id
350
351     def head(self, url, **kwargs):
352         return self.client_request("HEAD", url, **kwargs)
353
354     def get(self, url, **kwargs):
355         return self.client_request("GET", url, **kwargs)
356
357     def post(self, url, **kwargs):
358         return self.client_request("POST", url, **kwargs)
359
360     def put(self, url, **kwargs):
361         return self.client_request("PUT", url, **kwargs)
362
363     def delete(self, url, **kwargs):
364         return self.client_request("DELETE", url, **kwargs)
365
366     def patch(self, url, **kwargs):
367         return self.client_request("PATCH", url, **kwargs)
368
369     @staticmethod
370     def get_class(api_name, version, version_map):
371         """Returns the client class for the requested API version
372
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
377         """
378         try:
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,
384                         'version': version,
385                         'version_map': ', '.join(version_map.keys())}
386             raise exceptions.UnsupportedVersion(msg)
387
388         return importutils.import_class(client_path)