
* Uses auth plugins - is still WIP but using a working one for now. Will have to re-visit this once it lands in keystone / oslo update. * Resource and Manager abstractions. * Python 3 compatability. * Split the API/Bindings and Shell code * Use entrypoints for versions * Mandatory options are now positional Example: libra <cmd> --id <id> is now libra <cmd> <id> Change-Id: I42275cc88be5689f040864e195e48c5ebacb2cea
367 lines
13 KiB
Python
367 lines
13 KiB
Python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
# Copyright 2010 Jacob Kaplan-Moss
|
|
# Copyright 2011 OpenStack Foundation
|
|
# Copyright 2011 Piston Cloud Computing, Inc.
|
|
# Copyright 2013 Alessio Ababilov
|
|
# Copyright 2013 Grid Dynamics
|
|
# Copyright 2013 OpenStack Foundation
|
|
# 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.
|
|
|
|
"""
|
|
OpenStack Client interface. Handles the REST calls and responses.
|
|
"""
|
|
|
|
# E0202: An attribute inherited from %s hide this method
|
|
# pylint: disable=E0202
|
|
|
|
import logging
|
|
import time
|
|
|
|
try:
|
|
import simplejson as json
|
|
except ImportError:
|
|
import json
|
|
|
|
import requests
|
|
|
|
from libraclient.openstack.common.apiclient import exceptions
|
|
from libraclient.openstack.common import importutils
|
|
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class HTTPClient(object):
|
|
"""
|
|
This client handles sending HTTP requests to OpenStack servers.
|
|
|
|
Features:
|
|
- share authentication information between several clients to different
|
|
services (e.g., for compute and image clients);
|
|
- reissue authentication request for expired tokens;
|
|
- encode/decode JSON bodies;
|
|
- raise exeptions on HTTP errors;
|
|
- pluggable authentication;
|
|
- store authentication information in a keyring;
|
|
- store time spent for requests;
|
|
- register clients for particular services, so one can use
|
|
`http_client.identity` or `http_client.compute`;
|
|
- log requests and responses in a format that is easy to copy-and-paste
|
|
into terminal and send the same request with curl.
|
|
"""
|
|
|
|
user_agent = "libraclient.openstack.common.apiclient"
|
|
|
|
def __init__(self,
|
|
auth_plugin,
|
|
region_name=None,
|
|
endpoint_type="publicURL",
|
|
original_ip=None,
|
|
verify=True,
|
|
cert=None,
|
|
timeout=None,
|
|
timings=False,
|
|
keyring_saver=None,
|
|
debug=False,
|
|
user_agent=None,
|
|
http=None):
|
|
self.auth_plugin = auth_plugin
|
|
|
|
self.endpoint_type = endpoint_type
|
|
self.region_name = region_name
|
|
|
|
self.original_ip = original_ip
|
|
self.timeout = timeout
|
|
self.verify = verify
|
|
self.cert = cert
|
|
|
|
self.keyring_saver = keyring_saver
|
|
self.debug = debug
|
|
self.user_agent = user_agent or self.user_agent
|
|
|
|
self.times = [] # [("item", starttime, endtime), ...]
|
|
self.timings = timings
|
|
|
|
# requests within the same session can reuse TCP connections from pool
|
|
self.http = http or requests.Session()
|
|
|
|
self.cached_token = None
|
|
|
|
def _http_log_req(self, method, url, kwargs):
|
|
if not self.debug:
|
|
return
|
|
|
|
string_parts = [
|
|
"curl -i",
|
|
"-X '%s'" % method,
|
|
"'%s'" % url,
|
|
]
|
|
|
|
for element in kwargs['headers']:
|
|
header = "-H '%s: %s'" % (element, kwargs['headers'][element])
|
|
string_parts.append(header)
|
|
|
|
_logger.debug("REQ: %s" % " ".join(string_parts))
|
|
if 'data' in kwargs:
|
|
_logger.debug("REQ BODY: %s\n" % (kwargs['data']))
|
|
|
|
def _http_log_resp(self, resp):
|
|
if not self.debug:
|
|
return
|
|
_logger.debug(
|
|
"RESP: [%s] %s\n",
|
|
resp.status_code,
|
|
resp.headers)
|
|
if resp._content_consumed:
|
|
_logger.debug(
|
|
"RESP BODY: %s\n",
|
|
resp.text)
|
|
|
|
def serialize(self, kwargs):
|
|
if kwargs.get('json') is not None:
|
|
kwargs['headers']['Content-Type'] = 'application/json'
|
|
kwargs['data'] = json.dumps(kwargs['json'])
|
|
try:
|
|
del kwargs['json']
|
|
except KeyError:
|
|
pass
|
|
|
|
def get_timings(self):
|
|
return self.times
|
|
|
|
def reset_timings(self):
|
|
self.times = []
|
|
|
|
def request(self, method, url, **kwargs):
|
|
"""Send an http request with the specified characteristics.
|
|
|
|
Wrapper around `requests.Session.request` to handle tasks such as
|
|
setting headers, JSON encoding/decoding, and error handling.
|
|
|
|
:param method: method of HTTP request
|
|
:param url: URL of HTTP request
|
|
:param kwargs: any other parameter that can be passed to
|
|
requests.Session.request (such as `headers`) or `json`
|
|
that will be encoded as JSON and used as `data` argument
|
|
"""
|
|
kwargs.setdefault("headers", kwargs.get("headers", {}))
|
|
kwargs["headers"]["User-Agent"] = self.user_agent
|
|
if self.original_ip:
|
|
kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
|
|
self.original_ip, self.user_agent)
|
|
if self.timeout is not None:
|
|
kwargs.setdefault("timeout", self.timeout)
|
|
kwargs.setdefault("verify", self.verify)
|
|
if self.cert is not None:
|
|
kwargs.setdefault("cert", self.cert)
|
|
self.serialize(kwargs)
|
|
|
|
self._http_log_req(method, url, kwargs)
|
|
if self.timings:
|
|
start_time = time.time()
|
|
resp = self.http.request(method, url, **kwargs)
|
|
if self.timings:
|
|
self.times.append(("%s %s" % (method, url),
|
|
start_time, time.time()))
|
|
self._http_log_resp(resp)
|
|
|
|
if resp.status_code >= 400:
|
|
_logger.debug(
|
|
"Request returned failure status: %s",
|
|
resp.status_code)
|
|
raise exceptions.from_response(resp, method, url)
|
|
|
|
return resp
|
|
|
|
@staticmethod
|
|
def concat_url(endpoint, url):
|
|
"""Concatenate endpoint and final URL.
|
|
|
|
E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to
|
|
"http://keystone/v2.0/tokens".
|
|
|
|
:param endpoint: the base URL
|
|
:param url: the final URL
|
|
"""
|
|
return "%s/%s" % (endpoint.rstrip("/"), url.strip("/"))
|
|
|
|
def client_request(self, client, method, url, **kwargs):
|
|
"""Send an http request using `client`'s endpoint and specified `url`.
|
|
|
|
If request was rejected as unauthorized (possibly because the token is
|
|
expired), issue one authorization attempt and send the request once
|
|
again.
|
|
|
|
:param client: instance of BaseClient descendant
|
|
:param method: method of HTTP request
|
|
:param url: URL of HTTP request
|
|
:param kwargs: any other parameter that can be passed to
|
|
' `HTTPClient.request`
|
|
"""
|
|
|
|
filter_args = {
|
|
"endpoint_type": client.endpoint_type or self.endpoint_type,
|
|
"service_type": client.service_type,
|
|
}
|
|
token, endpoint = (self.cached_token, client.cached_endpoint)
|
|
just_authenticated = False
|
|
if not (token and endpoint):
|
|
try:
|
|
token, endpoint = self.auth_plugin.token_and_endpoint(
|
|
**filter_args)
|
|
except exceptions.EndpointException:
|
|
pass
|
|
if not (token and endpoint):
|
|
self.authenticate()
|
|
just_authenticated = True
|
|
token, endpoint = self.auth_plugin.token_and_endpoint(
|
|
**filter_args)
|
|
if not (token and endpoint):
|
|
raise exceptions.AuthorizationFailure(
|
|
"Cannot find endpoint or token for request")
|
|
|
|
old_token_endpoint = (token, endpoint)
|
|
kwargs.setdefault("headers", {})["X-Auth-Token"] = token
|
|
self.cached_token = token
|
|
client.cached_endpoint = endpoint
|
|
# Perform the request once. If we get Unauthorized, then it
|
|
# might be because the auth token expired, so try to
|
|
# re-authenticate and try again. If it still fails, bail.
|
|
try:
|
|
return self.request(
|
|
method, self.concat_url(endpoint, url), **kwargs)
|
|
except exceptions.Unauthorized as unauth_ex:
|
|
if just_authenticated:
|
|
raise
|
|
self.cached_token = None
|
|
client.cached_endpoint = None
|
|
self.authenticate()
|
|
try:
|
|
token, endpoint = self.auth_plugin.token_and_endpoint(
|
|
**filter_args)
|
|
except exceptions.EndpointException:
|
|
raise unauth_ex
|
|
if (not (token and endpoint) or
|
|
old_token_endpoint == (token, endpoint)):
|
|
raise unauth_ex
|
|
self.cached_token = token
|
|
client.cached_endpoint = endpoint
|
|
kwargs["headers"]["X-Auth-Token"] = token
|
|
return self.request(
|
|
method, self.concat_url(endpoint, url), **kwargs)
|
|
|
|
def add_client(self, base_client_instance):
|
|
"""Add a new instance of :class:`BaseClient` descendant.
|
|
|
|
`self` will store a reference to `base_client_instance`.
|
|
|
|
Example:
|
|
|
|
>>> def test_clients():
|
|
... from keystoneclient.auth import keystone
|
|
... from openstack.common.apiclient import client
|
|
... auth = keystone.KeystoneAuthPlugin(
|
|
... username="user", password="pass", tenant_name="tenant",
|
|
... auth_url="http://auth:5000/v2.0")
|
|
... openstack_client = client.HTTPClient(auth)
|
|
... # create nova client
|
|
... from novaclient.v1_1 import client
|
|
... client.Client(openstack_client)
|
|
... # create keystone client
|
|
... from keystoneclient.v2_0 import client
|
|
... client.Client(openstack_client)
|
|
... # use them
|
|
... openstack_client.identity.tenants.list()
|
|
... openstack_client.compute.servers.list()
|
|
"""
|
|
service_type = base_client_instance.service_type
|
|
if service_type and not hasattr(self, service_type):
|
|
setattr(self, service_type, base_client_instance)
|
|
|
|
def authenticate(self):
|
|
self.auth_plugin.authenticate(self)
|
|
# Store the authentication results in the keyring for later requests
|
|
if self.keyring_saver:
|
|
self.keyring_saver.save(self)
|
|
|
|
|
|
class BaseClient(object):
|
|
"""Top-level object to access the OpenStack API.
|
|
|
|
This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient`
|
|
will handle a bunch of issues such as authentication.
|
|
"""
|
|
|
|
service_type = None
|
|
endpoint_type = None # "publicURL" will be used
|
|
cached_endpoint = None
|
|
|
|
def __init__(self, http_client, service_type=None, endpoint_type=None,
|
|
extensions=None):
|
|
|
|
self.service_type = service_type
|
|
self.endpoint_type = endpoint_type
|
|
|
|
self.http_client = http_client
|
|
http_client.add_client(self)
|
|
|
|
# Add in any extensions...
|
|
if extensions:
|
|
for extension in extensions:
|
|
if extension.manager_class:
|
|
setattr(self, extension.name,
|
|
extension.manager_class(self))
|
|
|
|
def client_request(self, method, url, **kwargs):
|
|
return self.http_client.client_request(
|
|
self, method, url, **kwargs)
|
|
|
|
def head(self, url, **kwargs):
|
|
return self.client_request("HEAD", url, **kwargs)
|
|
|
|
def get(self, url, **kwargs):
|
|
return self.client_request("GET", url, **kwargs)
|
|
|
|
def post(self, url, **kwargs):
|
|
return self.client_request("POST", url, **kwargs)
|
|
|
|
def put(self, url, **kwargs):
|
|
return self.client_request("PUT", url, **kwargs)
|
|
|
|
def delete(self, url, **kwargs):
|
|
return self.client_request("DELETE", url, **kwargs)
|
|
|
|
def patch(self, url, **kwargs):
|
|
return self.client_request("PATCH", url, **kwargs)
|
|
|
|
@staticmethod
|
|
def get_class(api_name, version, version_map):
|
|
"""Returns the client class for the requested API version
|
|
|
|
:param api_name: the name of the API, e.g. 'compute', 'image', etc
|
|
:param version: the requested API version
|
|
:param version_map: a dict of client classes keyed by version
|
|
:rtype: a client class for the requested API version
|
|
"""
|
|
try:
|
|
client_path = version_map[str(version)]
|
|
except (KeyError, ValueError):
|
|
msg = "Invalid %s client version '%s'. must be one of: %s" % (
|
|
(api_name, version, ', '.join(version_map.keys())))
|
|
raise exceptions.UnsupportedVersion(msg)
|
|
|
|
return importutils.import_class(client_path)
|