~ubuntu-branches/ubuntu/vivid/ironic/vivid-updates

« back to all changes in this revision

Viewing changes to ironic/openstack/common/apiclient/client.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short
  • Date: 2015-03-30 11:14:57 UTC
  • mfrom: (1.2.6)
  • Revision ID: package-import@ubuntu.com-20150330111457-kr4ju3guf22m4vbz
Tags: 2015.1~b3-0ubuntu1
* New upstream release.
  + d/control: 
    - Align with upstream dependencies.
    - Add dh-python to build-dependencies.
    - Add psmisc as a dependency. (LP: #1358820)
  + d/p/fix-requirements.patch: Rediffed.
  + d/ironic-conductor.init.in: Fixed typos in LSB headers,
    thanks to JJ Asghar. (LP: #1429962)

Show diffs side-by-side

added added

removed removed

Lines of Context:
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 logging
29
 
import time
30
 
 
31
 
try:
32
 
    import simplejson as json
33
 
except ImportError:
34
 
    import json
35
 
 
36
 
import requests
37
 
 
38
 
from ironic.openstack.common.apiclient import exceptions
39
 
from ironic.openstack.common.gettextutils import _
40
 
from ironic.openstack.common import importutils
41
 
 
42
 
 
43
 
_logger = logging.getLogger(__name__)
44
 
 
45
 
 
46
 
class HTTPClient(object):
47
 
    """This client handles sending HTTP requests to OpenStack servers.
48
 
 
49
 
    Features:
50
 
 
51
 
    - share authentication information between several clients to different
52
 
      services (e.g., for compute and image clients);
53
 
    - reissue authentication request for expired tokens;
54
 
    - encode/decode JSON bodies;
55
 
    - raise exceptions on HTTP errors;
56
 
    - pluggable authentication;
57
 
    - store authentication information in a keyring;
58
 
    - store time spent for requests;
59
 
    - register clients for particular services, so one can use
60
 
      `http_client.identity` or `http_client.compute`;
61
 
    - log requests and responses in a format that is easy to copy-and-paste
62
 
      into terminal and send the same request with curl.
63
 
    """
64
 
 
65
 
    user_agent = "ironic.openstack.common.apiclient"
66
 
 
67
 
    def __init__(self,
68
 
                 auth_plugin,
69
 
                 region_name=None,
70
 
                 endpoint_type="publicURL",
71
 
                 original_ip=None,
72
 
                 verify=True,
73
 
                 cert=None,
74
 
                 timeout=None,
75
 
                 timings=False,
76
 
                 keyring_saver=None,
77
 
                 debug=False,
78
 
                 user_agent=None,
79
 
                 http=None):
80
 
        self.auth_plugin = auth_plugin
81
 
 
82
 
        self.endpoint_type = endpoint_type
83
 
        self.region_name = region_name
84
 
 
85
 
        self.original_ip = original_ip
86
 
        self.timeout = timeout
87
 
        self.verify = verify
88
 
        self.cert = cert
89
 
 
90
 
        self.keyring_saver = keyring_saver
91
 
        self.debug = debug
92
 
        self.user_agent = user_agent or self.user_agent
93
 
 
94
 
        self.times = []  # [("item", starttime, endtime), ...]
95
 
        self.timings = timings
96
 
 
97
 
        # requests within the same session can reuse TCP connections from pool
98
 
        self.http = http or requests.Session()
99
 
 
100
 
        self.cached_token = None
101
 
 
102
 
    def _http_log_req(self, method, url, kwargs):
103
 
        if not self.debug:
104
 
            return
105
 
 
106
 
        string_parts = [
107
 
            "curl -i",
108
 
            "-X '%s'" % method,
109
 
            "'%s'" % url,
110
 
        ]
111
 
 
112
 
        for element in kwargs['headers']:
113
 
            header = "-H '%s: %s'" % (element, kwargs['headers'][element])
114
 
            string_parts.append(header)
115
 
 
116
 
        _logger.debug("REQ: %s" % " ".join(string_parts))
117
 
        if 'data' in kwargs:
118
 
            _logger.debug("REQ BODY: %s\n" % (kwargs['data']))
119
 
 
120
 
    def _http_log_resp(self, resp):
121
 
        if not self.debug:
122
 
            return
123
 
        _logger.debug(
124
 
            "RESP: [%s] %s\n",
125
 
            resp.status_code,
126
 
            resp.headers)
127
 
        if resp._content_consumed:
128
 
            _logger.debug(
129
 
                "RESP BODY: %s\n",
130
 
                resp.text)
131
 
 
132
 
    def serialize(self, kwargs):
133
 
        if kwargs.get('json') is not None:
134
 
            kwargs['headers']['Content-Type'] = 'application/json'
135
 
            kwargs['data'] = json.dumps(kwargs['json'])
136
 
        try:
137
 
            del kwargs['json']
138
 
        except KeyError:
139
 
            pass
140
 
 
141
 
    def get_timings(self):
142
 
        return self.times
143
 
 
144
 
    def reset_timings(self):
145
 
        self.times = []
146
 
 
147
 
    def request(self, method, url, **kwargs):
148
 
        """Send an http request with the specified characteristics.
149
 
 
150
 
        Wrapper around `requests.Session.request` to handle tasks such as
151
 
        setting headers, JSON encoding/decoding, and error handling.
152
 
 
153
 
        :param method: method of HTTP request
154
 
        :param url: URL of HTTP request
155
 
        :param kwargs: any other parameter that can be passed to
156
 
             requests.Session.request (such as `headers`) or `json`
157
 
             that will be encoded as JSON and used as `data` argument
158
 
        """
159
 
        kwargs.setdefault("headers", kwargs.get("headers", {}))
160
 
        kwargs["headers"]["User-Agent"] = self.user_agent
161
 
        if self.original_ip:
162
 
            kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
163
 
                self.original_ip, self.user_agent)
164
 
        if self.timeout is not None:
165
 
            kwargs.setdefault("timeout", self.timeout)
166
 
        kwargs.setdefault("verify", self.verify)
167
 
        if self.cert is not None:
168
 
            kwargs.setdefault("cert", self.cert)
169
 
        self.serialize(kwargs)
170
 
 
171
 
        self._http_log_req(method, url, kwargs)
172
 
        if self.timings:
173
 
            start_time = time.time()
174
 
        resp = self.http.request(method, url, **kwargs)
175
 
        if self.timings:
176
 
            self.times.append(("%s %s" % (method, url),
177
 
                               start_time, time.time()))
178
 
        self._http_log_resp(resp)
179
 
 
180
 
        if resp.status_code >= 400:
181
 
            _logger.debug(
182
 
                "Request returned failure status: %s",
183
 
                resp.status_code)
184
 
            raise exceptions.from_response(resp, method, url)
185
 
 
186
 
        return resp
187
 
 
188
 
    @staticmethod
189
 
    def concat_url(endpoint, url):
190
 
        """Concatenate endpoint and final URL.
191
 
 
192
 
        E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to
193
 
        "http://keystone/v2.0/tokens".
194
 
 
195
 
        :param endpoint: the base URL
196
 
        :param url: the final URL
197
 
        """
198
 
        return "%s/%s" % (endpoint.rstrip("/"), url.strip("/"))
199
 
 
200
 
    def client_request(self, client, method, url, **kwargs):
201
 
        """Send an http request using `client`'s endpoint and specified `url`.
202
 
 
203
 
        If request was rejected as unauthorized (possibly because the token is
204
 
        expired), issue one authorization attempt and send the request once
205
 
        again.
206
 
 
207
 
        :param client: instance of BaseClient descendant
208
 
        :param method: method of HTTP request
209
 
        :param url: URL of HTTP request
210
 
        :param kwargs: any other parameter that can be passed to
211
 
            `HTTPClient.request`
212
 
        """
213
 
 
214
 
        filter_args = {
215
 
            "endpoint_type": client.endpoint_type or self.endpoint_type,
216
 
            "service_type": client.service_type,
217
 
        }
218
 
        token, endpoint = (self.cached_token, client.cached_endpoint)
219
 
        just_authenticated = False
220
 
        if not (token and endpoint):
221
 
            try:
222
 
                token, endpoint = self.auth_plugin.token_and_endpoint(
223
 
                    **filter_args)
224
 
            except exceptions.EndpointException:
225
 
                pass
226
 
            if not (token and endpoint):
227
 
                self.authenticate()
228
 
                just_authenticated = True
229
 
                token, endpoint = self.auth_plugin.token_and_endpoint(
230
 
                    **filter_args)
231
 
                if not (token and endpoint):
232
 
                    raise exceptions.AuthorizationFailure(
233
 
                        _("Cannot find endpoint or token for request"))
234
 
 
235
 
        old_token_endpoint = (token, endpoint)
236
 
        kwargs.setdefault("headers", {})["X-Auth-Token"] = token
237
 
        self.cached_token = token
238
 
        client.cached_endpoint = endpoint
239
 
        # Perform the request once. If we get Unauthorized, then it
240
 
        # might be because the auth token expired, so try to
241
 
        # re-authenticate and try again. If it still fails, bail.
242
 
        try:
243
 
            return self.request(
244
 
                method, self.concat_url(endpoint, url), **kwargs)
245
 
        except exceptions.Unauthorized as unauth_ex:
246
 
            if just_authenticated:
247
 
                raise
248
 
            self.cached_token = None
249
 
            client.cached_endpoint = None
250
 
            self.authenticate()
251
 
            try:
252
 
                token, endpoint = self.auth_plugin.token_and_endpoint(
253
 
                    **filter_args)
254
 
            except exceptions.EndpointException:
255
 
                raise unauth_ex
256
 
            if (not (token and endpoint) or
257
 
                    old_token_endpoint == (token, endpoint)):
258
 
                raise unauth_ex
259
 
            self.cached_token = token
260
 
            client.cached_endpoint = endpoint
261
 
            kwargs["headers"]["X-Auth-Token"] = token
262
 
            return self.request(
263
 
                method, self.concat_url(endpoint, url), **kwargs)
264
 
 
265
 
    def add_client(self, base_client_instance):
266
 
        """Add a new instance of :class:`BaseClient` descendant.
267
 
 
268
 
        `self` will store a reference to `base_client_instance`.
269
 
 
270
 
        Example:
271
 
 
272
 
        >>> def test_clients():
273
 
        ...     from keystoneclient.auth import keystone
274
 
        ...     from openstack.common.apiclient import client
275
 
        ...     auth = keystone.KeystoneAuthPlugin(
276
 
        ...         username="user", password="pass", tenant_name="tenant",
277
 
        ...         auth_url="http://auth:5000/v2.0")
278
 
        ...     openstack_client = client.HTTPClient(auth)
279
 
        ...     # create nova client
280
 
        ...     from novaclient.v1_1 import client
281
 
        ...     client.Client(openstack_client)
282
 
        ...     # create keystone client
283
 
        ...     from keystoneclient.v2_0 import client
284
 
        ...     client.Client(openstack_client)
285
 
        ...     # use them
286
 
        ...     openstack_client.identity.tenants.list()
287
 
        ...     openstack_client.compute.servers.list()
288
 
        """
289
 
        service_type = base_client_instance.service_type
290
 
        if service_type and not hasattr(self, service_type):
291
 
            setattr(self, service_type, base_client_instance)
292
 
 
293
 
    def authenticate(self):
294
 
        self.auth_plugin.authenticate(self)
295
 
        # Store the authentication results in the keyring for later requests
296
 
        if self.keyring_saver:
297
 
            self.keyring_saver.save(self)
298
 
 
299
 
 
300
 
class BaseClient(object):
301
 
    """Top-level object to access the OpenStack API.
302
 
 
303
 
    This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient`
304
 
    will handle a bunch of issues such as authentication.
305
 
    """
306
 
 
307
 
    service_type = None
308
 
    endpoint_type = None  # "publicURL" will be used
309
 
    cached_endpoint = None
310
 
 
311
 
    def __init__(self, http_client, extensions=None):
312
 
        self.http_client = http_client
313
 
        http_client.add_client(self)
314
 
 
315
 
        # Add in any extensions...
316
 
        if extensions:
317
 
            for extension in extensions:
318
 
                if extension.manager_class:
319
 
                    setattr(self, extension.name,
320
 
                            extension.manager_class(self))
321
 
 
322
 
    def client_request(self, method, url, **kwargs):
323
 
        return self.http_client.client_request(
324
 
            self, method, url, **kwargs)
325
 
 
326
 
    def head(self, url, **kwargs):
327
 
        return self.client_request("HEAD", url, **kwargs)
328
 
 
329
 
    def get(self, url, **kwargs):
330
 
        return self.client_request("GET", url, **kwargs)
331
 
 
332
 
    def post(self, url, **kwargs):
333
 
        return self.client_request("POST", url, **kwargs)
334
 
 
335
 
    def put(self, url, **kwargs):
336
 
        return self.client_request("PUT", url, **kwargs)
337
 
 
338
 
    def delete(self, url, **kwargs):
339
 
        return self.client_request("DELETE", url, **kwargs)
340
 
 
341
 
    def patch(self, url, **kwargs):
342
 
        return self.client_request("PATCH", url, **kwargs)
343
 
 
344
 
    @staticmethod
345
 
    def get_class(api_name, version, version_map):
346
 
        """Returns the client class for the requested API version
347
 
 
348
 
        :param api_name: the name of the API, e.g. 'compute', 'image', etc
349
 
        :param version: the requested API version
350
 
        :param version_map: a dict of client classes keyed by version
351
 
        :rtype: a client class for the requested API version
352
 
        """
353
 
        try:
354
 
            client_path = version_map[str(version)]
355
 
        except (KeyError, ValueError):
356
 
            msg = _("Invalid %(api_name)s client version '%(version)s'. "
357
 
                    "Must be one of: %(version_map)s") % {
358
 
                        'api_name': api_name,
359
 
                        'version': version,
360
 
                        'version_map': ', '.join(version_map.keys())
361
 
                    }
362
 
            raise exceptions.UnsupportedVersion(msg)
363
 
 
364
 
        return importutils.import_class(client_path)