base framework for cyborg client.
Include osc and httpclient lib codes. In order to avoid reinvent the wheel, most of libs leverage other openstack projects. Use this command to list accelerators: # openstack accelerator list ref: 1. osc plugin https://docs.openstack.org/python-openstackclient/latest/contributor/plugins.html# Change-Id: I90e39267eca3e91ce30807e00525cf637211e29d
This commit is contained in:
parent
fa63561e4a
commit
516a37b52d
0
cyborgclient/common/__init__.py
Normal file
0
cyborgclient/common/__init__.py
Normal file
0
cyborgclient/common/apiclient/__init__.py
Normal file
0
cyborgclient/common/apiclient/__init__.py
Normal file
108
cyborgclient/common/apiclient/base.py
Normal file
108
cyborgclient/common/apiclient/base.py
Normal file
@ -0,0 +1,108 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# Copyright 2012 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.
|
||||
|
||||
"""
|
||||
Base utilities to build API operation managers and objects on top of.
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
|
||||
class Resource(object):
|
||||
"""Base class for OpenStack resources (tenant, user, etc.).
|
||||
|
||||
This is pretty much just a bag for attributes.
|
||||
"""
|
||||
|
||||
def __init__(self, manager, info, loaded=False):
|
||||
"""Populate and bind to a manager.
|
||||
|
||||
:param manager: BaseManager object
|
||||
:param info: dictionary representing resource attributes
|
||||
:param loaded: prevent lazy-loading if set to True
|
||||
"""
|
||||
self.manager = manager
|
||||
self._info = info
|
||||
self._add_details(info)
|
||||
self._loaded = loaded
|
||||
|
||||
def __repr__(self):
|
||||
reprkeys = sorted(k
|
||||
for k in self.__dict__.keys()
|
||||
if k[0] != '_' and k != 'manager')
|
||||
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
|
||||
return "<%s %s>" % (self.__class__.__name__, info)
|
||||
|
||||
def _add_details(self, info):
|
||||
for (k, v) in info.items():
|
||||
try:
|
||||
setattr(self, k, v)
|
||||
self._info[k] = v
|
||||
except AttributeError:
|
||||
# In this case we already defined the attribute on the class
|
||||
pass
|
||||
|
||||
def __getattr__(self, k):
|
||||
if k not in self.__dict__:
|
||||
# NOTE(bcwaldon): disallow lazy-loading if already loaded once
|
||||
if not self.is_loaded():
|
||||
self.get()
|
||||
return self.__getattr__(k)
|
||||
|
||||
raise AttributeError(k)
|
||||
else:
|
||||
return self.__dict__[k]
|
||||
|
||||
def get(self):
|
||||
"""Support for lazy loading details.
|
||||
|
||||
Some clients, such as novaclient have the option to lazy load the
|
||||
details, details which can be loaded with this function.
|
||||
"""
|
||||
# set_loaded() first ... so if we have to bail, we know we tried.
|
||||
self.set_loaded(True)
|
||||
if not hasattr(self.manager, 'get'):
|
||||
return
|
||||
|
||||
new = self.manager.get(self.id)
|
||||
if new:
|
||||
self._add_details(new._info)
|
||||
self._add_details(
|
||||
{'x_request_id': self.manager.client.last_request_id})
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Resource):
|
||||
return NotImplemented
|
||||
# two resources of different types are not equal
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
if hasattr(self, 'id') and hasattr(other, 'id'):
|
||||
return self.id == other.id
|
||||
return self._info == other._info
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def is_loaded(self):
|
||||
return self._loaded
|
||||
|
||||
def set_loaded(self, val):
|
||||
self._loaded = val
|
||||
|
||||
def to_dict(self):
|
||||
return copy.deepcopy(self._info)
|
460
cyborgclient/common/apiclient/exceptions.py
Normal file
460
cyborgclient/common/apiclient/exceptions.py
Normal file
@ -0,0 +1,460 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 Nebula, Inc.
|
||||
# Copyright 2013 Alessio Ababilov
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Exception definitions.
|
||||
"""
|
||||
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
from cyborgclient.i18n import _
|
||||
|
||||
|
||||
class ClientException(Exception):
|
||||
"""The base exception class for all exceptions this library raises."""
|
||||
pass
|
||||
|
||||
|
||||
class ValidationError(ClientException):
|
||||
"""Error in validation on API client side."""
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedVersion(ClientException):
|
||||
"""User is trying to use an unsupported version of the API."""
|
||||
pass
|
||||
|
||||
|
||||
class CommandError(ClientException):
|
||||
"""Error in CLI tool."""
|
||||
pass
|
||||
|
||||
|
||||
class AuthorizationFailure(ClientException):
|
||||
"""Cannot authorize API client."""
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionError(ClientException):
|
||||
"""Cannot connect to API service."""
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionRefused(ConnectionError):
|
||||
"""Connection refused while trying to connect to API service."""
|
||||
pass
|
||||
|
||||
|
||||
class AuthPluginOptionsMissing(AuthorizationFailure):
|
||||
"""Auth plugin misses some options."""
|
||||
def __init__(self, opt_names):
|
||||
super(AuthPluginOptionsMissing, self).__init__(
|
||||
_("Authentication failed. Missing options: %s") %
|
||||
", ".join(opt_names))
|
||||
self.opt_names = opt_names
|
||||
|
||||
|
||||
class AuthSystemNotFound(AuthorizationFailure):
|
||||
"""User has specified an AuthSystem that is not installed."""
|
||||
def __init__(self, auth_system):
|
||||
super(AuthSystemNotFound, self).__init__(
|
||||
_("AuthSystemNotFound: %r") % auth_system)
|
||||
self.auth_system = auth_system
|
||||
|
||||
|
||||
class EndpointException(ClientException):
|
||||
"""Something is rotten in Service Catalog."""
|
||||
pass
|
||||
|
||||
|
||||
class EndpointNotFound(EndpointException):
|
||||
"""Could not find requested endpoint in Service Catalog."""
|
||||
pass
|
||||
|
||||
|
||||
class AmbiguousEndpoints(EndpointException):
|
||||
"""Found more than one matching endpoint in Service Catalog."""
|
||||
def __init__(self, endpoints=None):
|
||||
super(AmbiguousEndpoints, self).__init__(
|
||||
_("AmbiguousEndpoints: %r") % endpoints)
|
||||
self.endpoints = endpoints
|
||||
|
||||
|
||||
class HttpError(ClientException):
|
||||
"""The base exception class for all HTTP exceptions."""
|
||||
http_status = 0
|
||||
message = _("HTTP Error")
|
||||
|
||||
def __init__(self, message=None, details=None,
|
||||
response=None, request_id=None,
|
||||
url=None, method=None, http_status=None):
|
||||
self.http_status = http_status or self.http_status
|
||||
self.message = message or self.message
|
||||
self.details = details
|
||||
self.request_id = request_id
|
||||
self.response = response
|
||||
self.url = url
|
||||
self.method = method
|
||||
formatted_string = "%s (HTTP %s)" % (self.message, self.http_status)
|
||||
if request_id:
|
||||
formatted_string += " (Request-ID: %s)" % request_id
|
||||
super(HttpError, self).__init__(formatted_string)
|
||||
|
||||
|
||||
class HTTPRedirection(HttpError):
|
||||
"""HTTP Redirection."""
|
||||
message = _("HTTP Redirection")
|
||||
|
||||
|
||||
class HTTPClientError(HttpError):
|
||||
"""Client-side HTTP error.
|
||||
|
||||
Exception for cases in which the client seems to have erred.
|
||||
"""
|
||||
message = _("HTTP Client Error")
|
||||
|
||||
|
||||
class HttpServerError(HttpError):
|
||||
"""Server-side HTTP error.
|
||||
|
||||
Exception for cases in which the server is aware that it has
|
||||
erred or is incapable of performing the request.
|
||||
"""
|
||||
message = _("HTTP Server Error")
|
||||
|
||||
|
||||
class MultipleChoices(HTTPRedirection):
|
||||
"""HTTP 300 - Multiple Choices.
|
||||
|
||||
Indicates multiple options for the resource that the client may follow.
|
||||
"""
|
||||
|
||||
http_status = 300
|
||||
message = _("Multiple Choices")
|
||||
|
||||
|
||||
class BadRequest(HTTPClientError):
|
||||
"""HTTP 400 - Bad Request.
|
||||
|
||||
The request cannot be fulfilled due to bad syntax.
|
||||
"""
|
||||
http_status = 400
|
||||
message = _("Bad Request")
|
||||
|
||||
|
||||
class Unauthorized(HTTPClientError):
|
||||
"""HTTP 401 - Unauthorized.
|
||||
|
||||
Similar to 403 Forbidden, but specifically for use when authentication
|
||||
is required and has failed or has not yet been provided.
|
||||
"""
|
||||
http_status = 401
|
||||
message = _("Unauthorized")
|
||||
|
||||
|
||||
class PaymentRequired(HTTPClientError):
|
||||
"""HTTP 402 - Payment Required.
|
||||
|
||||
Reserved for future use.
|
||||
"""
|
||||
http_status = 402
|
||||
message = _("Payment Required")
|
||||
|
||||
|
||||
class Forbidden(HTTPClientError):
|
||||
"""HTTP 403 - Forbidden.
|
||||
|
||||
The request was a valid request, but the server is refusing to respond
|
||||
to it.
|
||||
"""
|
||||
http_status = 403
|
||||
message = _("Forbidden")
|
||||
|
||||
|
||||
class NotFound(HTTPClientError):
|
||||
"""HTTP 404 - Not Found.
|
||||
|
||||
The requested resource could not be found but may be available again
|
||||
in the future.
|
||||
"""
|
||||
http_status = 404
|
||||
message = _("Not Found")
|
||||
|
||||
|
||||
class MethodNotAllowed(HTTPClientError):
|
||||
"""HTTP 405 - Method Not Allowed.
|
||||
|
||||
A request was made of a resource using a request method not supported
|
||||
by that resource.
|
||||
"""
|
||||
http_status = 405
|
||||
message = _("Method Not Allowed")
|
||||
|
||||
|
||||
class NotAcceptable(HTTPClientError):
|
||||
"""HTTP 406 - Not Acceptable.
|
||||
|
||||
The requested resource is only capable of generating content not
|
||||
acceptable according to the Accept headers sent in the request.
|
||||
"""
|
||||
http_status = 406
|
||||
message = _("Not Acceptable")
|
||||
|
||||
|
||||
class ProxyAuthenticationRequired(HTTPClientError):
|
||||
"""HTTP 407 - Proxy Authentication Required.
|
||||
|
||||
The client must first authenticate itself with the proxy.
|
||||
"""
|
||||
http_status = 407
|
||||
message = _("Proxy Authentication Required")
|
||||
|
||||
|
||||
class RequestTimeout(HTTPClientError):
|
||||
"""HTTP 408 - Request Timeout.
|
||||
|
||||
The server timed out waiting for the request.
|
||||
"""
|
||||
http_status = 408
|
||||
message = _("Request Timeout")
|
||||
|
||||
|
||||
class Conflict(HTTPClientError):
|
||||
"""HTTP 409 - Conflict.
|
||||
|
||||
Indicates that the request could not be processed because of conflict
|
||||
in the request, such as an edit conflict.
|
||||
"""
|
||||
http_status = 409
|
||||
message = _("Conflict")
|
||||
|
||||
|
||||
class Gone(HTTPClientError):
|
||||
"""HTTP 410 - Gone.
|
||||
|
||||
Indicates that the resource requested is no longer available and will
|
||||
not be available again.
|
||||
"""
|
||||
http_status = 410
|
||||
message = _("Gone")
|
||||
|
||||
|
||||
class LengthRequired(HTTPClientError):
|
||||
"""HTTP 411 - Length Required.
|
||||
|
||||
The request did not specify the length of its content, which is
|
||||
required by the requested resource.
|
||||
"""
|
||||
http_status = 411
|
||||
message = _("Length Required")
|
||||
|
||||
|
||||
class PreconditionFailed(HTTPClientError):
|
||||
"""HTTP 412 - Precondition Failed.
|
||||
|
||||
The server does not meet one of the preconditions that the requester
|
||||
put on the request.
|
||||
"""
|
||||
http_status = 412
|
||||
message = _("Precondition Failed")
|
||||
|
||||
|
||||
class RequestEntityTooLarge(HTTPClientError):
|
||||
"""HTTP 413 - Request Entity Too Large.
|
||||
|
||||
The request is larger than the server is willing or able to process.
|
||||
"""
|
||||
http_status = 413
|
||||
message = _("Request Entity Too Large")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
try:
|
||||
self.retry_after = int(kwargs.pop('retry_after'))
|
||||
except (KeyError, ValueError):
|
||||
self.retry_after = 0
|
||||
|
||||
super(RequestEntityTooLarge, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class RequestUriTooLong(HTTPClientError):
|
||||
"""HTTP 414 - Request-URI Too Long.
|
||||
|
||||
The URI provided was too long for the server to process.
|
||||
"""
|
||||
http_status = 414
|
||||
message = _("Request-URI Too Long")
|
||||
|
||||
|
||||
class UnsupportedMediaType(HTTPClientError):
|
||||
"""HTTP 415 - Unsupported Media Type.
|
||||
|
||||
The request entity has a media type which the server or resource does
|
||||
not support.
|
||||
"""
|
||||
http_status = 415
|
||||
message = _("Unsupported Media Type")
|
||||
|
||||
|
||||
class RequestedRangeNotSatisfiable(HTTPClientError):
|
||||
"""HTTP 416 - Requested Range Not Satisfiable.
|
||||
|
||||
The client has asked for a portion of the file, but the server cannot
|
||||
supply that portion.
|
||||
"""
|
||||
http_status = 416
|
||||
message = _("Requested Range Not Satisfiable")
|
||||
|
||||
|
||||
class ExpectationFailed(HTTPClientError):
|
||||
"""HTTP 417 - Expectation Failed.
|
||||
|
||||
The server cannot meet the requirements of the Expect request-header field.
|
||||
"""
|
||||
http_status = 417
|
||||
message = _("Expectation Failed")
|
||||
|
||||
|
||||
class UnprocessableEntity(HTTPClientError):
|
||||
"""HTTP 422 - Unprocessable Entity.
|
||||
|
||||
The request was well-formed but was unable to be followed due to semantic
|
||||
errors.
|
||||
"""
|
||||
http_status = 422
|
||||
message = _("Unprocessable Entity")
|
||||
|
||||
|
||||
class InternalServerError(HttpServerError):
|
||||
"""HTTP 500 - Internal Server Error.
|
||||
|
||||
A generic error message, given when no more specific message is suitable.
|
||||
"""
|
||||
http_status = 500
|
||||
message = _("Internal Server Error")
|
||||
|
||||
|
||||
# NotImplemented is a python keyword.
|
||||
class HttpNotImplemented(HttpServerError):
|
||||
"""HTTP 501 - Not Implemented.
|
||||
|
||||
The server either does not recognize the request method, or it lacks
|
||||
the ability to fulfill the request.
|
||||
"""
|
||||
http_status = 501
|
||||
message = _("Not Implemented")
|
||||
|
||||
|
||||
class BadGateway(HttpServerError):
|
||||
"""HTTP 502 - Bad Gateway.
|
||||
|
||||
The server was acting as a gateway or proxy and received an invalid
|
||||
response from the upstream server.
|
||||
"""
|
||||
http_status = 502
|
||||
message = _("Bad Gateway")
|
||||
|
||||
|
||||
class ServiceUnavailable(HttpServerError):
|
||||
"""HTTP 503 - Service Unavailable.
|
||||
|
||||
The server is currently unavailable.
|
||||
"""
|
||||
http_status = 503
|
||||
message = _("Service Unavailable")
|
||||
|
||||
|
||||
class GatewayTimeout(HttpServerError):
|
||||
"""HTTP 504 - Gateway Timeout.
|
||||
|
||||
The server was acting as a gateway or proxy and did not receive a timely
|
||||
response from the upstream server.
|
||||
"""
|
||||
http_status = 504
|
||||
message = _("Gateway Timeout")
|
||||
|
||||
|
||||
class HttpVersionNotSupported(HttpServerError):
|
||||
"""HTTP 505 - HttpVersion Not Supported.
|
||||
|
||||
The server does not support the HTTP protocol version used in the request.
|
||||
"""
|
||||
http_status = 505
|
||||
message = _("HTTP Version Not Supported")
|
||||
|
||||
|
||||
# _code_map contains all the classes that have http_status attribute.
|
||||
_code_map = dict(
|
||||
(getattr(obj, 'http_status', None), obj)
|
||||
for name, obj in vars(sys.modules[__name__]).items()
|
||||
if inspect.isclass(obj) and getattr(obj, 'http_status', False)
|
||||
)
|
||||
|
||||
|
||||
def from_response(response, method, url):
|
||||
"""Returns an instance of :class:`HttpError` or subclass based on response.
|
||||
|
||||
:param response: instance of `requests.Response` class
|
||||
:param method: HTTP method used for request
|
||||
:param url: URL used for request
|
||||
"""
|
||||
|
||||
req_id = response.headers.get("x-openstack-request-id")
|
||||
# NOTE(hdd) true for older versions of nova and cinder
|
||||
if not req_id:
|
||||
req_id = response.headers.get("x-compute-request-id")
|
||||
kwargs = {
|
||||
"http_status": response.status_code,
|
||||
"response": response,
|
||||
"method": method,
|
||||
"url": url,
|
||||
"request_id": req_id,
|
||||
}
|
||||
if "retry-after" in response.headers:
|
||||
kwargs["retry_after"] = response.headers["retry-after"]
|
||||
|
||||
content_type = response.headers.get("Content-Type", "")
|
||||
if content_type.startswith("application/json"):
|
||||
try:
|
||||
body = response.json()
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
if isinstance(body, dict):
|
||||
error = body.get(list(body)[0])
|
||||
if isinstance(error, dict):
|
||||
kwargs["message"] = (error.get("message") or
|
||||
error.get("faultstring"))
|
||||
kwargs["details"] = (error.get("details") or
|
||||
six.text_type(body))
|
||||
elif content_type.startswith("text/"):
|
||||
kwargs["details"] = getattr(response, 'text', '')
|
||||
|
||||
try:
|
||||
cls = _code_map[response.status_code]
|
||||
except KeyError:
|
||||
if 500 <= response.status_code < 600:
|
||||
cls = HttpServerError
|
||||
elif 400 <= response.status_code < 500:
|
||||
cls = HTTPClientError
|
||||
else:
|
||||
cls = HttpError
|
||||
return cls(**kwargs)
|
149
cyborgclient/common/base.py
Normal file
149
cyborgclient/common/base.py
Normal file
@ -0,0 +1,149 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2012 OpenStack LLC.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Base utilities to build API operation managers and objects on top of.
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
||||
from cyborgclient.common.apiclient import base
|
||||
|
||||
|
||||
def getid(obj):
|
||||
"""Wrapper to get object's ID.
|
||||
|
||||
Abstracts the common pattern of allowing both an object or an
|
||||
object's ID (UUID) as a parameter when dealing with relationships.
|
||||
"""
|
||||
try:
|
||||
return obj.id
|
||||
except AttributeError:
|
||||
return obj
|
||||
|
||||
|
||||
class Manager(object):
|
||||
"""Provides CRUD operations with a particular API."""
|
||||
resource_class = None
|
||||
|
||||
def __init__(self, api):
|
||||
self.api = api
|
||||
|
||||
def _create(self, url, body):
|
||||
resp, body = self.api.json_request('POST', url, body=body)
|
||||
if body:
|
||||
return self.resource_class(self, body)
|
||||
|
||||
def _format_body_data(self, body, response_key):
|
||||
if response_key:
|
||||
try:
|
||||
data = body[response_key]
|
||||
except KeyError:
|
||||
return []
|
||||
else:
|
||||
data = body
|
||||
|
||||
if not isinstance(data, list):
|
||||
data = [data]
|
||||
|
||||
return data
|
||||
|
||||
def _list_pagination(self, url, response_key=None, obj_class=None,
|
||||
limit=None):
|
||||
"""Retrieve a list of items.
|
||||
|
||||
The Cyborg API is configured to return a maximum number of
|
||||
items per request, (FIXME: see Cyborg's api.max_limit option). This
|
||||
iterates over the 'next' link (pagination) in the responses,
|
||||
to get the number of items specified by 'limit'. If 'limit'
|
||||
is None this function will continue pagination until there are
|
||||
no more values to be returned.
|
||||
|
||||
:param url: a partial URL, e.g. '/nodes'
|
||||
:param response_key: the key to be looked up in response
|
||||
dictionary, e.g. 'nodes'
|
||||
:param obj_class: class for constructing the returned objects.
|
||||
:param limit: maximum number of items to return. If None returns
|
||||
everything.
|
||||
|
||||
"""
|
||||
if obj_class is None:
|
||||
obj_class = self.resource_class
|
||||
|
||||
if limit is not None:
|
||||
limit = int(limit)
|
||||
|
||||
object_list = []
|
||||
object_count = 0
|
||||
limit_reached = False
|
||||
while url:
|
||||
resp, body = self.api.json_request('GET', url)
|
||||
data = self._format_body_data(body, response_key)
|
||||
for obj in data:
|
||||
object_list.append(obj_class(self, obj, loaded=True))
|
||||
object_count += 1
|
||||
if limit and object_count >= limit:
|
||||
# break the for loop
|
||||
limit_reached = True
|
||||
break
|
||||
|
||||
# break the while loop and return
|
||||
if limit_reached:
|
||||
break
|
||||
|
||||
url = body.get('next')
|
||||
if url:
|
||||
# NOTE(lucasagomes): We need to edit the URL to remove
|
||||
# the scheme and netloc
|
||||
url_parts = list(urlparse.urlparse(url))
|
||||
url_parts[0] = url_parts[1] = ''
|
||||
url = urlparse.urlunparse(url_parts)
|
||||
|
||||
return object_list
|
||||
|
||||
def _list(self, url, response_key=None, obj_class=None, body=None):
|
||||
resp, body = self.api.json_request('GET', url)
|
||||
|
||||
if obj_class is None:
|
||||
obj_class = self.resource_class
|
||||
|
||||
data = self._format_body_data(body, response_key)
|
||||
return [obj_class(self, res, loaded=True) for res in data if res]
|
||||
|
||||
def _update(self, url, body=None, method='PATCH', response_key=None):
|
||||
if body:
|
||||
resp, resp_body = self.api.json_request(method, url, body=body)
|
||||
else:
|
||||
resp, resp_body = self.api.raw_request(method, url)
|
||||
# PATCH/PUT requests may not return a body
|
||||
if resp_body:
|
||||
return self.resource_class(self, resp_body)
|
||||
|
||||
def _delete(self, url):
|
||||
self.api.raw_request('DELETE', url)
|
||||
|
||||
|
||||
class Resource(base.Resource):
|
||||
"""Represents a particular instance of an object (tenant, user, etc).
|
||||
|
||||
This is pretty much just a bag for attributes.
|
||||
"""
|
||||
|
||||
def to_dict(self):
|
||||
return copy.deepcopy(self._info)
|
508
cyborgclient/common/cliutils.py
Normal file
508
cyborgclient/common/cliutils.py
Normal file
@ -0,0 +1,508 @@
|
||||
# Copyright 2012 Red Hat, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
# W0603: Using the global statement
|
||||
# W0621: Redefining name %s from outer scope
|
||||
# pylint: disable=W0603,W0621
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import getpass
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
import decorator
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import strutils
|
||||
import prettytable
|
||||
import six
|
||||
from six import moves
|
||||
|
||||
from cyborgclient.common.apiclient import exceptions
|
||||
from cyborgclient.i18n import _
|
||||
|
||||
|
||||
DEPRECATION_BASE = ('%sThe --%s parameter is deprecated and '
|
||||
'will be removed in a future release. Use the '
|
||||
'<%s> positional parameter %s.')
|
||||
|
||||
NAME_DEPRECATION_HELP = DEPRECATION_BASE % ('', 'name', 'name', 'instead')
|
||||
|
||||
NAME_DEPRECATION_WARNING = DEPRECATION_BASE % (
|
||||
'WARNING: ', 'name', 'name', 'to avoid seeing this message')
|
||||
|
||||
CLUSTER_DEPRECATION_HELP = DEPRECATION_BASE % ('', 'cluster', 'cluster',
|
||||
'instead')
|
||||
|
||||
CLUSTER_DEPRECATION_WARNING = DEPRECATION_BASE % (
|
||||
'WARNING: ', 'cluster', 'cluster', 'to avoid seeing this message')
|
||||
|
||||
CYBORG_CLIENT_DEPRECATION_WARNING = (
|
||||
'WARNING: The cyborg client is deprecated and will be removed in a future '
|
||||
'release.\nUse the OpenStack client to avoid seeing this message.')
|
||||
|
||||
|
||||
def deprecation_message(preamble, new_name):
|
||||
msg = ('%s This parameter is deprecated and will be removed in a future '
|
||||
'release. Use --%s instead.' % (preamble, new_name))
|
||||
return msg
|
||||
|
||||
|
||||
class MissingArgs(Exception):
|
||||
"""Supplied arguments are not sufficient for calling a function."""
|
||||
def __init__(self, missing):
|
||||
self.missing = missing
|
||||
msg = _("Missing arguments: %s") % ", ".join(missing)
|
||||
super(MissingArgs, self).__init__(msg)
|
||||
|
||||
|
||||
class DuplicateArgs(Exception):
|
||||
"""More than one of the same argument type was passed."""
|
||||
def __init__(self, param, dupes):
|
||||
msg = _('Duplicate "%(param)s" arguments: %(dupes)s') % {
|
||||
'param': param, 'dupes': ", ".join(dupes)}
|
||||
super(DuplicateArgs, self).__init__(msg)
|
||||
|
||||
|
||||
def validate_args(fn, *args, **kwargs):
|
||||
"""Check that the supplied args are sufficient for calling a function.
|
||||
|
||||
>>> validate_args(lambda a: None)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
MissingArgs: Missing argument(s): a
|
||||
>>> validate_args(lambda a, b, c, d: None, 0, c=1)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
MissingArgs: Missing argument(s): b, d
|
||||
|
||||
:param fn: the function to check
|
||||
:param arg: the positional arguments supplied
|
||||
:param kwargs: the keyword arguments supplied
|
||||
"""
|
||||
argspec = inspect.getargspec(fn)
|
||||
|
||||
num_defaults = len(argspec.defaults or [])
|
||||
required_args = argspec.args[:len(argspec.args) - num_defaults]
|
||||
|
||||
def isbound(method):
|
||||
return getattr(method, '__self__', None) is not None
|
||||
|
||||
if isbound(fn):
|
||||
required_args.pop(0)
|
||||
|
||||
missing = [arg for arg in required_args if arg not in kwargs]
|
||||
missing = missing[len(args):]
|
||||
if missing:
|
||||
raise MissingArgs(missing)
|
||||
|
||||
|
||||
def validate_name_args(positional_name, optional_name):
|
||||
if optional_name:
|
||||
print(NAME_DEPRECATION_WARNING)
|
||||
if positional_name and optional_name:
|
||||
raise DuplicateArgs("<name>", (positional_name, optional_name))
|
||||
|
||||
|
||||
def validate_cluster_args(positional_cluster, optional_cluster):
|
||||
if optional_cluster:
|
||||
print(CLUSTER_DEPRECATION_WARNING)
|
||||
if positional_cluster and optional_cluster:
|
||||
raise DuplicateArgs("<cluster>", (positional_cluster,
|
||||
optional_cluster))
|
||||
|
||||
|
||||
def deprecated(message):
|
||||
"""Decorator for marking a call as deprecated by printing a given message.
|
||||
|
||||
Example:
|
||||
>>> @deprecated("Bay functions are deprecated and should be replaced by "
|
||||
... "calls to cluster")
|
||||
... def bay_create(args):
|
||||
... pass
|
||||
"""
|
||||
@decorator.decorator
|
||||
def wrapper(func, *args, **kwargs):
|
||||
print(message)
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def deprecation_map(dep_map):
|
||||
"""Decorator for applying a map of deprecating arguments to a function.
|
||||
|
||||
The map connects deprecating arguments and their replacements. The
|
||||
shell.py script uses this map to create mutually exclusive argument groups
|
||||
in argparse and also prints a deprecation warning telling the user to
|
||||
switch to the updated argument.
|
||||
|
||||
NOTE: This decorator MUST be the outermost in the chain of argument
|
||||
decorators to work correctly.
|
||||
|
||||
Example usage:
|
||||
>>> @deprecation_map({ "old-argument": "new-argument" })
|
||||
... @args("old-argument", required=True)
|
||||
... @args("new-argument", required=True)
|
||||
... def do_command_line_stuff():
|
||||
... pass
|
||||
"""
|
||||
def _decorator(func):
|
||||
if not hasattr(func, 'arguments'):
|
||||
return func
|
||||
|
||||
func.deprecated_groups = []
|
||||
for old_param, new_param in dep_map.items():
|
||||
old_info, new_info = None, None
|
||||
required = False
|
||||
for (args, kwargs) in func.arguments:
|
||||
if old_param in args:
|
||||
old_info = (args, kwargs)
|
||||
# Old arguments shouldn't be required if they were not
|
||||
# previously, so prioritize old requirement
|
||||
if 'required' in kwargs:
|
||||
required = kwargs['required']
|
||||
# Set to false so argparse doesn't get angry
|
||||
kwargs['required'] = False
|
||||
elif new_param in args:
|
||||
new_info = (args, kwargs)
|
||||
kwargs['required'] = False
|
||||
if old_info and new_info:
|
||||
break
|
||||
# Add a tuple of (old, new, required), which in turn is:
|
||||
# ((old_args, old_kwargs), (new_args, new_kwargs), required)
|
||||
func.deprecated_groups.append((old_info, new_info, required))
|
||||
# Remove arguments that would be duplicated by the groups we made
|
||||
func.arguments.remove(old_info)
|
||||
func.arguments.remove(new_info)
|
||||
|
||||
return func
|
||||
return _decorator
|
||||
|
||||
|
||||
def arg(*args, **kwargs):
|
||||
"""Decorator for CLI args.
|
||||
|
||||
Example:
|
||||
|
||||
>>> @arg("name", help="Name of the new entity")
|
||||
... def entity_create(args):
|
||||
... pass
|
||||
"""
|
||||
def _decorator(func):
|
||||
add_arg(func, *args, **kwargs)
|
||||
return func
|
||||
return _decorator
|
||||
|
||||
|
||||
def env(*args, **kwargs):
|
||||
"""Returns the first environment variable set.
|
||||
|
||||
If all are empty, defaults to '' or keyword arg `default`.
|
||||
"""
|
||||
for arg in args:
|
||||
value = os.environ.get(arg)
|
||||
if value:
|
||||
return value
|
||||
return kwargs.get('default', '')
|
||||
|
||||
|
||||
def add_arg(func, *args, **kwargs):
|
||||
"""Bind CLI arguments to a shell.py `do_foo` function."""
|
||||
|
||||
if not hasattr(func, 'arguments'):
|
||||
func.arguments = []
|
||||
|
||||
# NOTE(sirp): avoid dups that can occur when the module is shared across
|
||||
# tests.
|
||||
if (args, kwargs) not in func.arguments:
|
||||
# Because of the semantics of decorator composition if we just append
|
||||
# to the options list positional options will appear to be backwards.
|
||||
func.arguments.insert(0, (args, kwargs))
|
||||
|
||||
|
||||
def unauthenticated(func):
|
||||
"""Adds 'unauthenticated' attribute to decorated function.
|
||||
|
||||
Usage:
|
||||
|
||||
>>> @unauthenticated
|
||||
... def mymethod(f):
|
||||
... pass
|
||||
"""
|
||||
func.unauthenticated = True
|
||||
return func
|
||||
|
||||
|
||||
def isunauthenticated(func):
|
||||
"""Checks if the function does not require authentication.
|
||||
|
||||
Mark such functions with the `@unauthenticated` decorator.
|
||||
|
||||
:returns: bool
|
||||
"""
|
||||
return getattr(func, 'unauthenticated', False)
|
||||
|
||||
|
||||
def print_list(objs, fields, formatters=None, sortby_index=0,
|
||||
mixed_case_fields=None, field_labels=None):
|
||||
"""Print a list or objects as a table, one row per object.
|
||||
|
||||
:param objs: iterable of :class:`Resource`
|
||||
:param fields: attributes that correspond to columns, in order
|
||||
:param formatters: `dict` of callables for field formatting
|
||||
:param sortby_index: index of the field for sorting table rows
|
||||
:param mixed_case_fields: fields corresponding to object attributes that
|
||||
have mixed case names (e.g., 'serverId')
|
||||
:param field_labels: Labels to use in the heading of the table, default to
|
||||
fields.
|
||||
"""
|
||||
formatters = formatters or {}
|
||||
mixed_case_fields = mixed_case_fields or []
|
||||
field_labels = field_labels or fields
|
||||
if len(field_labels) != len(fields):
|
||||
raise ValueError(_("Field labels list %(labels)s has different number "
|
||||
"of elements than fields list %(fields)s"),
|
||||
{'labels': field_labels, 'fields': fields})
|
||||
|
||||
if sortby_index is None:
|
||||
kwargs = {}
|
||||
else:
|
||||
kwargs = {'sortby': field_labels[sortby_index]}
|
||||
pt = prettytable.PrettyTable(field_labels)
|
||||
pt.align = 'l'
|
||||
|
||||
for o in objs:
|
||||
row = []
|
||||
for field in fields:
|
||||
data = '-'
|
||||
if field in formatters:
|
||||
data = formatters[field](o)
|
||||
else:
|
||||
if field in mixed_case_fields:
|
||||
field_name = field.replace(' ', '_')
|
||||
else:
|
||||
field_name = field.lower().replace(' ', '_')
|
||||
data = getattr(o, field_name, '')
|
||||
if data is None:
|
||||
data = '-'
|
||||
row.append(data)
|
||||
pt.add_row(row)
|
||||
|
||||
if six.PY3:
|
||||
print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode())
|
||||
else:
|
||||
print(encodeutils.safe_encode(pt.get_string(**kwargs)))
|
||||
|
||||
|
||||
def keys_and_vals_to_strs(dictionary):
|
||||
"""Recursively convert a dictionary's keys and values to strings.
|
||||
|
||||
:param dictionary: dictionary whose keys/vals are to be converted to strs
|
||||
"""
|
||||
def to_str(k_or_v):
|
||||
if isinstance(k_or_v, dict):
|
||||
return keys_and_vals_to_strs(k_or_v)
|
||||
elif isinstance(k_or_v, six.text_type):
|
||||
return str(k_or_v)
|
||||
else:
|
||||
return k_or_v
|
||||
return dict((to_str(k), to_str(v)) for k, v in dictionary.items())
|
||||
|
||||
|
||||
def print_dict(dct, dict_property="Property", wrap=0):
|
||||
"""Print a `dict` as a table of two columns.
|
||||
|
||||
:param dct: `dict` to print
|
||||
:param dict_property: name of the first column
|
||||
:param wrap: wrapping for the second column
|
||||
"""
|
||||
pt = prettytable.PrettyTable([dict_property, 'Value'])
|
||||
pt.align = 'l'
|
||||
for k, v in dct.items():
|
||||
# convert dict to str to check length
|
||||
if isinstance(v, dict):
|
||||
v = six.text_type(keys_and_vals_to_strs(v))
|
||||
if wrap > 0:
|
||||
v = textwrap.fill(six.text_type(v), wrap)
|
||||
# if value has a newline, add in multiple rows
|
||||
# e.g. fault with stacktrace
|
||||
if v and isinstance(v, six.string_types) and r'\n' in v:
|
||||
lines = v.strip().split(r'\n')
|
||||
col1 = k
|
||||
for line in lines:
|
||||
pt.add_row([col1, line])
|
||||
col1 = ''
|
||||
elif isinstance(v, list):
|
||||
val = str([str(i) for i in v])
|
||||
if val is None:
|
||||
val = '-'
|
||||
pt.add_row([k, val])
|
||||
else:
|
||||
if v is None:
|
||||
v = '-'
|
||||
pt.add_row([k, v])
|
||||
|
||||
if six.PY3:
|
||||
print(encodeutils.safe_encode(pt.get_string()).decode())
|
||||
else:
|
||||
print(encodeutils.safe_encode(pt.get_string()))
|
||||
|
||||
|
||||
def get_password(max_password_prompts=3):
|
||||
"""Read password from TTY."""
|
||||
verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD"))
|
||||
pw = None
|
||||
if hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
|
||||
# Check for Ctrl-D
|
||||
try:
|
||||
for __ in moves.range(max_password_prompts):
|
||||
pw1 = getpass.getpass("OS Password: ")
|
||||
if verify:
|
||||
pw2 = getpass.getpass("Please verify: ")
|
||||
else:
|
||||
pw2 = pw1
|
||||
if pw1 == pw2 and pw1:
|
||||
pw = pw1
|
||||
break
|
||||
except EOFError:
|
||||
pass
|
||||
return pw
|
||||
|
||||
|
||||
def service_type(stype):
|
||||
"""Adds 'service_type' attribute to decorated function.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@service_type('volume')
|
||||
def mymethod(f):
|
||||
...
|
||||
"""
|
||||
def inner(f):
|
||||
f.service_type = stype
|
||||
return f
|
||||
return inner
|
||||
|
||||
|
||||
def get_service_type(f):
|
||||
"""Retrieves service type from function."""
|
||||
return getattr(f, 'service_type', None)
|
||||
|
||||
|
||||
def pretty_choice_list(l):
|
||||
return ', '.join("'%s'" % i for i in l)
|
||||
|
||||
|
||||
def exit(msg=''):
|
||||
if msg:
|
||||
print(msg, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _format_field_name(attr):
|
||||
"""Format an object attribute in a human-friendly way."""
|
||||
# Split at ':' and leave the extension name as-is.
|
||||
parts = attr.rsplit(':', 1)
|
||||
name = parts[-1].replace('_', ' ')
|
||||
# Don't title() on mixed case
|
||||
if name.isupper() or name.islower():
|
||||
name = name.title()
|
||||
parts[-1] = name
|
||||
return ': '.join(parts)
|
||||
|
||||
|
||||
def make_field_formatter(attr, filters=None):
|
||||
"""Given an object attribute.
|
||||
|
||||
Return a formatted field name and a formatter suitable for passing to
|
||||
print_list.
|
||||
Optionally pass a dict mapping attribute names to a function. The function
|
||||
will be passed the value of the attribute and should return the string to
|
||||
display.
|
||||
"""
|
||||
|
||||
filter_ = None
|
||||
if filters:
|
||||
filter_ = filters.get(attr)
|
||||
|
||||
def get_field(obj):
|
||||
field = getattr(obj, attr, '')
|
||||
if field and filter_:
|
||||
field = filter_(field)
|
||||
return field
|
||||
|
||||
name = _format_field_name(attr)
|
||||
formatter = get_field
|
||||
return name, formatter
|
||||
|
||||
|
||||
def _get_list_table_columns_and_formatters(fields, objs, exclude_fields=(),
|
||||
filters=None):
|
||||
"""Check and add fields to output columns.
|
||||
|
||||
If there is any value in fields that not an attribute of obj,
|
||||
CommandError will be raised.
|
||||
If fields has duplicate values (case sensitive), we will make them unique
|
||||
and ignore duplicate ones.
|
||||
:param fields: A list of string contains the fields to be printed.
|
||||
:param objs: An list of object which will be used to check if field is
|
||||
valid or not. Note, we don't check fields if obj is None or
|
||||
empty.
|
||||
:param exclude_fields: A tuple of string which contains the fields to be
|
||||
excluded.
|
||||
:param filters: A dictionary defines how to get value from fields, this
|
||||
is useful when field's value is a complex object such as
|
||||
dictionary.
|
||||
:return: columns, formatters.
|
||||
columns is a list of string which will be used as table header.
|
||||
formatters is a dictionary specifies how to display the value
|
||||
of the field.
|
||||
They can be [], {}.
|
||||
:raise: cyborgclient.common.apiclient.exceptions.CommandError.
|
||||
"""
|
||||
|
||||
if objs and isinstance(objs, list):
|
||||
obj = objs[0]
|
||||
else:
|
||||
obj = None
|
||||
fields = None
|
||||
|
||||
columns = []
|
||||
formatters = {}
|
||||
|
||||
if fields:
|
||||
non_existent_fields = []
|
||||
exclude_fields = set(exclude_fields)
|
||||
|
||||
for field in fields.split(','):
|
||||
if not hasattr(obj, field):
|
||||
non_existent_fields.append(field)
|
||||
continue
|
||||
if field in exclude_fields:
|
||||
continue
|
||||
field_title, formatter = make_field_formatter(field, filters)
|
||||
columns.append(field_title)
|
||||
formatters[field_title] = formatter
|
||||
exclude_fields.add(field)
|
||||
|
||||
if non_existent_fields:
|
||||
raise exceptions.CommandError(
|
||||
_("Non-existent fields are specified: %s") %
|
||||
non_existent_fields
|
||||
)
|
||||
return columns, formatters
|
347
cyborgclient/common/http.py
Normal file
347
cyborgclient/common/http.py
Normal file
@ -0,0 +1,347 @@
|
||||
# Copyright 2016 Huawei, 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 copy
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
|
||||
from keystoneauth1 import adapter
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import importutils
|
||||
import requests
|
||||
import six
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from cyborgclient.common import exceptions as exc
|
||||
from cyborgclient.common import utils
|
||||
from cyborgclient.i18n import _
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
USER_AGENT = 'python-cyborgclient'
|
||||
CHUNKSIZE = 1024 * 64 # 64kB
|
||||
SENSITIVE_HEADERS = ('X-Auth-Token',)
|
||||
osprofiler_web = importutils.try_import('osprofiler.web')
|
||||
|
||||
|
||||
def get_system_ca_file():
|
||||
"""Return path to system default CA file."""
|
||||
# Standard CA file locations for Debian/Ubuntu, RedHat/Fedora,
|
||||
# Suse, FreeBSD/OpenBSD, MacOSX, and the bundled ca
|
||||
ca_path = ['/etc/ssl/certs/ca-certificates.crt',
|
||||
'/etc/pki/tls/certs/ca-bundle.crt',
|
||||
'/etc/ssl/ca-bundle.pem',
|
||||
'/etc/ssl/cert.pem',
|
||||
'/System/Library/OpenSSL/certs/cacert.pem',
|
||||
requests.certs.where()]
|
||||
for ca in ca_path:
|
||||
LOG.debug("Looking for ca file %s", ca)
|
||||
if os.path.exists(ca):
|
||||
LOG.debug("Using ca file %s", ca)
|
||||
return ca
|
||||
LOG.warning("System ca file could not be found.")
|
||||
|
||||
|
||||
class HTTPClient(object):
|
||||
|
||||
def __init__(self, endpoint, **kwargs):
|
||||
self.endpoint = endpoint
|
||||
self.auth_url = kwargs.get('auth_url')
|
||||
self.auth_token = kwargs.get('token')
|
||||
self.username = kwargs.get('username')
|
||||
self.password = kwargs.get('password')
|
||||
self.region_name = kwargs.get('region_name')
|
||||
self.include_pass = kwargs.get('include_pass')
|
||||
self.endpoint_url = endpoint
|
||||
|
||||
self.cert_file = kwargs.get('cert_file')
|
||||
self.key_file = kwargs.get('key_file')
|
||||
self.timeout = kwargs.get('timeout')
|
||||
|
||||
self.ssl_connection_params = {
|
||||
'ca_file': kwargs.get('ca_file'),
|
||||
'cert_file': kwargs.get('cert_file'),
|
||||
'key_file': kwargs.get('key_file'),
|
||||
'insecure': kwargs.get('insecure'),
|
||||
}
|
||||
|
||||
self.verify_cert = None
|
||||
if parse.urlparse(endpoint).scheme == "https":
|
||||
if kwargs.get('insecure'):
|
||||
self.verify_cert = False
|
||||
else:
|
||||
self.verify_cert = kwargs.get('ca_file', get_system_ca_file())
|
||||
|
||||
# FIXME(RuiChen): We need this for compatibility with the oslo
|
||||
# apiclient we should move to inheriting this class from the oslo
|
||||
# HTTPClient
|
||||
self.last_request_id = None
|
||||
|
||||
def safe_header(self, name, value):
|
||||
if name in SENSITIVE_HEADERS:
|
||||
# because in python3 byte string handling is ... ug
|
||||
v = value.encode('utf-8')
|
||||
h = hashlib.sha1(v)
|
||||
d = h.hexdigest()
|
||||
return encodeutils.safe_decode(name), "{SHA1}%s" % d
|
||||
else:
|
||||
return (encodeutils.safe_decode(name),
|
||||
encodeutils.safe_decode(value))
|
||||
|
||||
def log_curl_request(self, method, url, kwargs):
|
||||
curl = ['curl -g -i -X %s' % method]
|
||||
|
||||
for (key, value) in kwargs['headers'].items():
|
||||
header = '-H \'%s: %s\'' % self.safe_header(key, value)
|
||||
curl.append(header)
|
||||
|
||||
conn_params_fmt = [
|
||||
('key_file', '--key %s'),
|
||||
('cert_file', '--cert %s'),
|
||||
('ca_file', '--cacert %s'),
|
||||
]
|
||||
for (key, fmt) in conn_params_fmt:
|
||||
value = self.ssl_connection_params.get(key)
|
||||
if value:
|
||||
curl.append(fmt % value)
|
||||
|
||||
if self.ssl_connection_params.get('insecure'):
|
||||
curl.append('-k')
|
||||
|
||||
if 'data' in kwargs:
|
||||
curl.append('-d \'%s\'' % kwargs['data'])
|
||||
|
||||
curl.append('%s%s' % (self.endpoint, url))
|
||||
LOG.debug(' '.join(curl))
|
||||
|
||||
@staticmethod
|
||||
def log_http_response(resp):
|
||||
status = (resp.raw.version / 10.0, resp.status_code, resp.reason)
|
||||
dump = ['\nHTTP/%.1f %s %s' % status]
|
||||
dump.extend(['%s: %s' % (k, v) for k, v in resp.headers.items()])
|
||||
dump.append('')
|
||||
if resp.content:
|
||||
content = resp.content
|
||||
if isinstance(content, six.binary_type):
|
||||
content = content.decode()
|
||||
dump.extend([content, ''])
|
||||
LOG.debug('\n'.join(dump))
|
||||
|
||||
def _http_request(self, url, method, **kwargs):
|
||||
"""Send an http request with the specified characteristics.
|
||||
|
||||
Wrapper around requests.request to handle tasks such as
|
||||
setting headers and error handling.
|
||||
"""
|
||||
# Copy the kwargs so we can reuse the original in case of redirects
|
||||
kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
|
||||
kwargs['headers'].setdefault('User-Agent', USER_AGENT)
|
||||
if self.auth_token:
|
||||
kwargs['headers'].setdefault('X-Auth-Token', self.auth_token)
|
||||
else:
|
||||
kwargs['headers'].update(self.credentials_headers())
|
||||
if self.auth_url:
|
||||
kwargs['headers'].setdefault('X-Auth-Url', self.auth_url)
|
||||
if self.region_name:
|
||||
kwargs['headers'].setdefault('X-Region-Name', self.region_name)
|
||||
if self.include_pass and 'X-Auth-Key' not in kwargs['headers']:
|
||||
kwargs['headers'].update(self.credentials_headers())
|
||||
if osprofiler_web:
|
||||
kwargs['headers'].update(osprofiler_web.get_trace_id_headers())
|
||||
|
||||
self.log_curl_request(method, url, kwargs)
|
||||
|
||||
if self.cert_file and self.key_file:
|
||||
kwargs['cert'] = (self.cert_file, self.key_file)
|
||||
|
||||
if self.verify_cert is not None:
|
||||
kwargs['verify'] = self.verify_cert
|
||||
|
||||
if self.timeout is not None:
|
||||
kwargs['timeout'] = float(self.timeout)
|
||||
|
||||
# Allow caller to specify not to follow redirects, in which case we
|
||||
# just return the redirect response. Useful for using stacks:lookup.
|
||||
redirect = kwargs.pop('redirect', True)
|
||||
|
||||
# Since requests does not follow the RFC when doing redirection to sent
|
||||
# back the same method on a redirect we are simply bypassing it. For
|
||||
# example if we do a DELETE/POST/PUT on a URL and we get a 302 RFC says
|
||||
# that we should follow that URL with the same method as before,
|
||||
# requests doesn't follow that and send a GET instead for the method.
|
||||
# Hopefully this could be fixed as they say in a comment in a future
|
||||
# point version i.e.: 3.x
|
||||
# See issue: https://github.com/kennethreitz/requests/issues/1704
|
||||
allow_redirects = False
|
||||
|
||||
# Use fully qualified URL from response header for redirects
|
||||
if not parse.urlparse(url).netloc:
|
||||
url = self.endpoint_url + url
|
||||
|
||||
try:
|
||||
resp = requests.request(
|
||||
method,
|
||||
url,
|
||||
allow_redirects=allow_redirects,
|
||||
**kwargs)
|
||||
except socket.gaierror as e:
|
||||
message = (_("Error finding address for %(url)s: %(e)s") %
|
||||
{'url': self.endpoint_url + url, 'e': e})
|
||||
raise exc.EndpointNotFound(message=message)
|
||||
except (socket.error, socket.timeout) as e:
|
||||
endpoint = self.endpoint
|
||||
message = (_("Error communicating with %(endpoint)s %(e)s") %
|
||||
{'endpoint': endpoint, 'e': e})
|
||||
raise exc.ConnectionError(message=message)
|
||||
|
||||
self.log_http_response(resp)
|
||||
|
||||
if not ('X-Auth-Key' in kwargs['headers']) and (
|
||||
resp.status_code == 401 or
|
||||
(resp.status_code == 500 and "(HTTP 401)" in resp.content)):
|
||||
raise exc.AuthorizationFailure(_("Authentication failed: %s")
|
||||
% resp.content)
|
||||
elif 400 <= resp.status_code < 600:
|
||||
raise exc.from_response(resp, method, url)
|
||||
elif resp.status_code in (301, 302, 305):
|
||||
# Redirected. Reissue the request to the new location,
|
||||
# unless caller specified redirect=False
|
||||
if redirect:
|
||||
location = resp.headers.get('location')
|
||||
location = self.strip_endpoint(location)
|
||||
resp = self._http_request(location, method, **kwargs)
|
||||
elif resp.status_code == 300:
|
||||
raise exc.from_response(resp, method, url)
|
||||
|
||||
return resp
|
||||
|
||||
def strip_endpoint(self, location):
|
||||
if location is None:
|
||||
message = _("Location not returned with redirect")
|
||||
raise exc.EndpointException(message=message)
|
||||
if location.lower().startswith(self.endpoint):
|
||||
return location[len(self.endpoint):]
|
||||
else:
|
||||
return location
|
||||
|
||||
def credentials_headers(self):
|
||||
creds = {}
|
||||
# NOTE(RuiChen): When deferred_auth_method=password, Heat
|
||||
# encrypts and stores username/password. For Keystone v3, the
|
||||
# intent is to use trusts since SHARDY is working towards
|
||||
# deferred_auth_method=trusts as the default.
|
||||
if self.username:
|
||||
creds['X-Auth-User'] = self.username
|
||||
if self.password:
|
||||
creds['X-Auth-Key'] = self.password
|
||||
return creds
|
||||
|
||||
def json_request(self, method, url, **kwargs):
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers'].setdefault('Content-Type', 'application/json')
|
||||
kwargs['headers'].setdefault('Accept', 'application/json')
|
||||
|
||||
if 'data' in kwargs:
|
||||
kwargs['data'] = jsonutils.dumps(kwargs['data'])
|
||||
|
||||
resp = self._http_request(url, method, **kwargs)
|
||||
body = utils.get_response_body(resp)
|
||||
return resp, body
|
||||
|
||||
def raw_request(self, method, url, **kwargs):
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers'].setdefault('Content-Type',
|
||||
'application/octet-stream')
|
||||
resp = self._http_request(url, method, **kwargs)
|
||||
body = utils.get_response_body(resp)
|
||||
return resp, body
|
||||
|
||||
def head(self, url, **kwargs):
|
||||
return self.json_request("HEAD", url, **kwargs)
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
return self.json_request("GET", url, **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self.json_request("POST", url, **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self.json_request("PUT", url, **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
return self.raw_request("DELETE", url, **kwargs)
|
||||
|
||||
def patch(self, url, **kwargs):
|
||||
return self.json_request("PATCH", url, **kwargs)
|
||||
|
||||
|
||||
class SessionClient(adapter.LegacyJsonAdapter):
|
||||
"""HTTP client based on Keystone client session."""
|
||||
|
||||
def request(self, url, method, **kwargs):
|
||||
redirect = kwargs.get('redirect')
|
||||
kwargs.setdefault('user_agent', USER_AGENT)
|
||||
|
||||
if 'data' in kwargs:
|
||||
kwargs['json'] = kwargs.pop('data')
|
||||
|
||||
resp, body = super(SessionClient, self).request(
|
||||
url, method,
|
||||
raise_exc=False,
|
||||
**kwargs)
|
||||
|
||||
if 400 <= resp.status_code < 600:
|
||||
raise exc.from_response(resp, method, url)
|
||||
elif resp.status_code in (301, 302, 305):
|
||||
if redirect:
|
||||
location = resp.headers.get('location')
|
||||
path = self.strip_endpoint(location)
|
||||
resp, body = self.request(path, method, **kwargs)
|
||||
elif resp.status_code == 300:
|
||||
raise exc.from_response(resp, method, url)
|
||||
|
||||
return resp, body
|
||||
|
||||
def credentials_headers(self):
|
||||
return {}
|
||||
|
||||
def strip_endpoint(self, location):
|
||||
if location is None:
|
||||
message = _("Location not returned with redirect")
|
||||
raise exc.EndpointException(message=message)
|
||||
if (self.endpoint_override is not None and
|
||||
location.lower().startswith(self.endpoint_override.lower())):
|
||||
return location[len(self.endpoint_override):]
|
||||
else:
|
||||
return location
|
||||
|
||||
|
||||
def _construct_http_client(endpoint=None, username=None, password=None,
|
||||
include_pass=None, endpoint_type=None,
|
||||
auth_url=None, **kwargs):
|
||||
session = kwargs.pop('session', None)
|
||||
auth = kwargs.pop('auth', None)
|
||||
|
||||
if session:
|
||||
kwargs['endpoint_override'] = endpoint
|
||||
return SessionClient(session, auth=auth, **kwargs)
|
||||
else:
|
||||
return HTTPClient(endpoint=endpoint, username=username,
|
||||
password=password, include_pass=include_pass,
|
||||
endpoint_type=endpoint_type, auth_url=auth_url,
|
||||
**kwargs)
|
432
cyborgclient/common/httpclient.py
Normal file
432
cyborgclient/common/httpclient.py
Normal file
@ -0,0 +1,432 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2012 OpenStack LLC.
|
||||
# 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 copy
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import ssl
|
||||
|
||||
from keystoneauth1 import adapter
|
||||
from oslo_utils import importutils
|
||||
import six
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
||||
from cyborgclient import exceptions
|
||||
|
||||
osprofiler_web = importutils.try_import("osprofiler.web")
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
USER_AGENT = 'python-cyborgclient'
|
||||
CHUNKSIZE = 1024 * 64 # 64kB
|
||||
|
||||
API_VERSION = '/v1'
|
||||
DEFAULT_API_VERSION = 'latest'
|
||||
|
||||
|
||||
def _extract_error_json(body):
|
||||
"""Return error_message from the HTTP response body."""
|
||||
error_json = {}
|
||||
try:
|
||||
body_json = json.loads(body)
|
||||
if 'error_message' in body_json:
|
||||
raw_msg = body_json['error_message']
|
||||
error_json = json.loads(raw_msg)
|
||||
elif 'error' in body_json:
|
||||
error_body = body_json['error']
|
||||
error_json = {'faultstring': error_body['title'],
|
||||
'debuginfo': error_body['message']}
|
||||
else:
|
||||
error_body = body_json['errors'][0]
|
||||
error_json = {'faultstring': error_body['title']}
|
||||
if 'detail' in error_body:
|
||||
error_json['debuginfo'] = error_body['detail']
|
||||
elif 'description' in error_body:
|
||||
error_json['debuginfo'] = error_body['description']
|
||||
|
||||
except ValueError:
|
||||
return {}
|
||||
|
||||
return error_json
|
||||
|
||||
|
||||
class HTTPClient(object):
|
||||
|
||||
def __init__(self, endpoint, api_version=DEFAULT_API_VERSION, **kwargs):
|
||||
self.endpoint = endpoint
|
||||
self.auth_token = kwargs.get('token')
|
||||
self.auth_ref = kwargs.get('auth_ref')
|
||||
self.api_version = api_version
|
||||
self.connection_params = self.get_connection_params(endpoint, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def get_connection_params(endpoint, **kwargs):
|
||||
parts = urlparse.urlparse(endpoint)
|
||||
|
||||
# trim API version and trailing slash from endpoint
|
||||
path = parts.path
|
||||
path = path.rstrip('/').rstrip(API_VERSION)
|
||||
|
||||
_args = (parts.hostname, parts.port, path)
|
||||
_kwargs = {'timeout': (float(kwargs.get('timeout'))
|
||||
if kwargs.get('timeout') else 600)}
|
||||
|
||||
if parts.scheme == 'https':
|
||||
_class = VerifiedHTTPSConnection
|
||||
_kwargs['ca_file'] = kwargs.get('ca_file', None)
|
||||
_kwargs['cert_file'] = kwargs.get('cert_file', None)
|
||||
_kwargs['key_file'] = kwargs.get('key_file', None)
|
||||
_kwargs['insecure'] = kwargs.get('insecure', False)
|
||||
elif parts.scheme == 'http':
|
||||
_class = six.moves.http_client.HTTPConnection
|
||||
else:
|
||||
msg = 'Unsupported scheme: %s' % parts.scheme
|
||||
raise exceptions.EndpointException(msg)
|
||||
|
||||
return (_class, _args, _kwargs)
|
||||
|
||||
def get_connection(self):
|
||||
_class = self.connection_params[0]
|
||||
return _class(*self.connection_params[1][0:2],
|
||||
**self.connection_params[2])
|
||||
|
||||
def log_curl_request(self, method, url, kwargs):
|
||||
curl = ['curl -i -X %s' % method]
|
||||
|
||||
for (key, value) in kwargs['headers'].items():
|
||||
header = '-H \'%s: %s\'' % (key, value)
|
||||
curl.append(header)
|
||||
|
||||
conn_params_fmt = [
|
||||
('key_file', '--key %s'),
|
||||
('cert_file', '--cert %s'),
|
||||
('ca_file', '--cacert %s'),
|
||||
]
|
||||
for (key, fmt) in conn_params_fmt:
|
||||
value = self.connection_params[2].get(key)
|
||||
if value:
|
||||
curl.append(fmt % value)
|
||||
|
||||
if self.connection_params[2].get('insecure'):
|
||||
curl.append('-k')
|
||||
|
||||
if 'body' in kwargs:
|
||||
curl.append('-d \'%s\'' % kwargs['body'])
|
||||
|
||||
curl.append('%s/%s' % (self.endpoint, url.lstrip(API_VERSION)))
|
||||
LOG.debug(' '.join(curl))
|
||||
|
||||
@staticmethod
|
||||
def log_http_response(resp, body=None):
|
||||
status = (resp.version / 10.0, resp.status, resp.reason)
|
||||
dump = ['\nHTTP/%.1f %s %s' % status]
|
||||
dump.extend(['%s: %s' % (k, v) for k, v in resp.getheaders()])
|
||||
dump.append('')
|
||||
if body:
|
||||
dump.extend([body, ''])
|
||||
LOG.debug('\n'.join(dump))
|
||||
|
||||
def _make_connection_url(self, url):
|
||||
(_class, _args, _kwargs) = self.connection_params
|
||||
base_url = _args[2]
|
||||
return '%s/%s' % (base_url, url.lstrip('/'))
|
||||
|
||||
def _http_request(self, url, method, **kwargs):
|
||||
"""Send an http request with the specified characteristics.
|
||||
|
||||
Wrapper around httplib.HTTP(S)Connection.request to handle tasks such
|
||||
as setting headers and error handling.
|
||||
"""
|
||||
# Copy the kwargs so we can reuse the original in case of redirects
|
||||
kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
|
||||
kwargs['headers'].setdefault('User-Agent', USER_AGENT)
|
||||
if self.api_version:
|
||||
version_string = 'container-infra %s' % self.api_version
|
||||
kwargs['headers'].setdefault(
|
||||
'OpenStack-API-Version', version_string)
|
||||
if self.auth_token:
|
||||
kwargs['headers'].setdefault('X-Auth-Token', self.auth_token)
|
||||
|
||||
self.log_curl_request(method, url, kwargs)
|
||||
conn = self.get_connection()
|
||||
|
||||
try:
|
||||
conn_url = self._make_connection_url(url)
|
||||
conn.request(method, conn_url, **kwargs)
|
||||
resp = conn.getresponse()
|
||||
except socket.gaierror as e:
|
||||
message = ("Error finding address for %(url)s: %(e)s"
|
||||
% dict(url=url, e=e))
|
||||
raise exceptions.EndpointNotFound(message)
|
||||
except (socket.error, socket.timeout) as e:
|
||||
endpoint = self.endpoint
|
||||
message = ("Error communicating with %(endpoint)s %(e)s"
|
||||
% dict(endpoint=endpoint, e=e))
|
||||
raise exceptions.ConnectionRefused(message)
|
||||
|
||||
body_iter = ResponseBodyIterator(resp)
|
||||
|
||||
# Read body into string if it isn't obviously image data
|
||||
body_str = None
|
||||
if resp.getheader('content-type', None) != 'application/octet-stream':
|
||||
# decoding byte to string is necessary for Python 3.4 compatibility
|
||||
# this issues has not been found with Python 3.4 unit tests
|
||||
# because the test creates a fake http response of type str
|
||||
# the if statement satisfies test (str) and real (bytes) behavior
|
||||
body_list = [
|
||||
chunk.decode("utf-8") if isinstance(chunk, bytes)
|
||||
else chunk for chunk in body_iter
|
||||
]
|
||||
body_str = ''.join(body_list)
|
||||
self.log_http_response(resp, body_str)
|
||||
body_iter = six.StringIO(body_str)
|
||||
else:
|
||||
self.log_http_response(resp)
|
||||
|
||||
if 400 <= resp.status < 600:
|
||||
LOG.warning("Request returned failure status.")
|
||||
error_json = _extract_error_json(body_str)
|
||||
raise exceptions.from_response(
|
||||
resp, method, url,
|
||||
error_json.get('faultstring'),
|
||||
error_json.get('debuginfo'))
|
||||
elif resp.status in (301, 302, 305):
|
||||
# Redirected. Reissue the request to the new location.
|
||||
return self._http_request(resp['location'], method, **kwargs)
|
||||
elif resp.status == 300:
|
||||
raise exceptions.from_response(resp, method=method, url=url)
|
||||
|
||||
return resp, body_iter
|
||||
|
||||
def json_request(self, method, url, **kwargs):
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers'].setdefault('Content-Type', 'application/json')
|
||||
kwargs['headers'].setdefault('Accept', 'application/json')
|
||||
|
||||
if 'body' in kwargs:
|
||||
kwargs['body'] = json.dumps(kwargs['body'])
|
||||
|
||||
resp, body_iter = self._http_request(url, method, **kwargs)
|
||||
content_type = resp.getheader('content-type', None)
|
||||
|
||||
if resp.status == 204 or resp.status == 205 or content_type is None:
|
||||
return resp, list()
|
||||
|
||||
if 'application/json' in content_type:
|
||||
body = ''.join([chunk for chunk in body_iter])
|
||||
try:
|
||||
body = json.loads(body)
|
||||
except ValueError:
|
||||
LOG.error('Could not decode response body as JSON')
|
||||
else:
|
||||
body = None
|
||||
|
||||
return resp, body
|
||||
|
||||
def raw_request(self, method, url, **kwargs):
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers'].setdefault('Content-Type',
|
||||
'application/octet-stream')
|
||||
return self._http_request(url, method, **kwargs)
|
||||
|
||||
|
||||
class VerifiedHTTPSConnection(six.moves.http_client.HTTPSConnection):
|
||||
"""httplib-compatibile connection using client-side SSL authentication
|
||||
|
||||
:see http://code.activestate.com/recipes/
|
||||
577548-https-httplib-client-connection-with-certificate-v/
|
||||
"""
|
||||
|
||||
def __init__(self, host, port, key_file=None, cert_file=None,
|
||||
ca_file=None, timeout=None, insecure=False):
|
||||
six.moves.http_client.HTTPSConnection.__init__(self, host, port,
|
||||
key_file=key_file,
|
||||
cert_file=cert_file)
|
||||
self.key_file = key_file
|
||||
self.cert_file = cert_file
|
||||
if ca_file is not None:
|
||||
self.ca_file = ca_file
|
||||
else:
|
||||
self.ca_file = self.get_system_ca_file()
|
||||
self.timeout = timeout
|
||||
self.insecure = insecure
|
||||
|
||||
def connect(self):
|
||||
"""Connect to a host on a given (SSL) port.
|
||||
|
||||
If ca_file is pointing somewhere, use it to check Server Certificate.
|
||||
|
||||
Redefined/copied and extended from httplib.py:1105 (Python 2.6.x).
|
||||
This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to
|
||||
ssl.wrap_socket(), which forces SSL to check server certificate against
|
||||
our client certificate.
|
||||
"""
|
||||
sock = socket.create_connection((self.host, self.port), self.timeout)
|
||||
|
||||
if self._tunnel_host:
|
||||
self.sock = sock
|
||||
self._tunnel()
|
||||
|
||||
if self.insecure is True:
|
||||
kwargs = {'cert_reqs': ssl.CERT_NONE}
|
||||
else:
|
||||
kwargs = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': self.ca_file}
|
||||
|
||||
if self.cert_file:
|
||||
kwargs['certfile'] = self.cert_file
|
||||
if self.key_file:
|
||||
kwargs['keyfile'] = self.key_file
|
||||
|
||||
self.sock = ssl.wrap_socket(sock, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def get_system_ca_file():
|
||||
"""Return path to system default CA file."""
|
||||
# Standard CA file locations for Debian/Ubuntu, RedHat/Fedora,
|
||||
# Suse, FreeBSD/OpenBSD
|
||||
ca_path = ['/etc/ssl/certs/ca-certificates.crt',
|
||||
'/etc/pki/tls/certs/ca-bundle.crt',
|
||||
'/etc/ssl/ca-bundle.pem',
|
||||
'/etc/ssl/cert.pem']
|
||||
for ca in ca_path:
|
||||
if os.path.exists(ca):
|
||||
return ca
|
||||
return None
|
||||
|
||||
|
||||
class SessionClient(adapter.LegacyJsonAdapter):
|
||||
"""HTTP client based on Keystone client session."""
|
||||
|
||||
def __init__(self, user_agent=USER_AGENT, logger=LOG,
|
||||
api_version=DEFAULT_API_VERSION, *args, **kwargs):
|
||||
self.user_agent = USER_AGENT
|
||||
self.api_version = api_version
|
||||
super(SessionClient, self).__init__(*args, **kwargs)
|
||||
|
||||
def _http_request(self, url, method, **kwargs):
|
||||
if url.startswith(API_VERSION):
|
||||
url = url[len(API_VERSION):]
|
||||
|
||||
kwargs.setdefault('user_agent', self.user_agent)
|
||||
kwargs.setdefault('auth', self.auth)
|
||||
kwargs.setdefault('endpoint_override', self.endpoint_override)
|
||||
|
||||
# Copy the kwargs so we can reuse the original in case of redirects
|
||||
kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
|
||||
kwargs['headers'].setdefault('User-Agent', self.user_agent)
|
||||
# NOTE(tovin07): osprofiler_web.get_trace_id_headers does not add any
|
||||
# headers in case if osprofiler is not initialized.
|
||||
if osprofiler_web:
|
||||
kwargs['headers'].update(osprofiler_web.get_trace_id_headers())
|
||||
if self.api_version:
|
||||
version_string = 'container-infra %s' % self.api_version
|
||||
kwargs['headers'].setdefault(
|
||||
'OpenStack-API-Version', version_string)
|
||||
|
||||
endpoint_filter = kwargs.setdefault('endpoint_filter', {})
|
||||
endpoint_filter.setdefault('interface', self.interface)
|
||||
endpoint_filter.setdefault('service_type', self.service_type)
|
||||
endpoint_filter.setdefault('region_name', self.region_name)
|
||||
|
||||
resp = self.session.request(url, method,
|
||||
raise_exc=False, **kwargs)
|
||||
|
||||
if 400 <= resp.status_code < 600:
|
||||
error_json = _extract_error_json(resp.content)
|
||||
raise exceptions.from_response(
|
||||
resp, method, url,
|
||||
error_json.get('faultstring'),
|
||||
error_json.get('debuginfo'))
|
||||
elif resp.status_code in (301, 302, 305):
|
||||
# Redirected. Reissue the request to the new location.
|
||||
location = resp.headers.get('location')
|
||||
resp = self._http_request(location, method, **kwargs)
|
||||
elif resp.status_code == 300:
|
||||
raise exceptions.from_response(resp, method=method, url=url)
|
||||
return resp
|
||||
|
||||
def json_request(self, method, url, **kwargs):
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers'].setdefault('Content-Type', 'application/json')
|
||||
kwargs['headers'].setdefault('Accept', 'application/json')
|
||||
if 'body' in kwargs:
|
||||
kwargs['data'] = json.dumps(kwargs.pop('body'))
|
||||
|
||||
resp = self._http_request(url, method, **kwargs)
|
||||
body = resp.content
|
||||
content_type = resp.headers.get('content-type', None)
|
||||
status = resp.status_code
|
||||
if status == 204 or status == 205 or content_type is None:
|
||||
return resp, list()
|
||||
if 'application/json' in content_type:
|
||||
try:
|
||||
body = resp.json()
|
||||
except ValueError:
|
||||
LOG.error('Could not decode response body as JSON')
|
||||
else:
|
||||
body = None
|
||||
|
||||
return resp, body
|
||||
|
||||
def raw_request(self, method, url, **kwargs):
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers'].setdefault('Content-Type',
|
||||
'application/octet-stream')
|
||||
return self._http_request(url, method, **kwargs)
|
||||
|
||||
|
||||
class ResponseBodyIterator(object):
|
||||
"""A class that acts as an iterator over an HTTP response."""
|
||||
|
||||
def __init__(self, resp):
|
||||
self.resp = resp
|
||||
|
||||
def __iter__(self):
|
||||
while True:
|
||||
yield self.next()
|
||||
|
||||
def __bool__(self):
|
||||
return hasattr(self, 'items')
|
||||
|
||||
__nonzero__ = __bool__ # Python 2.x compatibility
|
||||
|
||||
def next(self):
|
||||
chunk = self.resp.read(CHUNKSIZE)
|
||||
if chunk:
|
||||
return chunk
|
||||
else:
|
||||
raise StopIteration()
|
||||
|
||||
|
||||
def _construct_http_client(*args, **kwargs):
|
||||
session = kwargs.pop('session', None)
|
||||
auth = kwargs.pop('auth', None)
|
||||
|
||||
if session:
|
||||
service_type = kwargs.pop('service_type', 'baremetal')
|
||||
interface = kwargs.pop('endpoint_type', None)
|
||||
region_name = kwargs.pop('region_name', None)
|
||||
return SessionClient(session=session,
|
||||
auth=auth,
|
||||
interface=interface,
|
||||
service_type=service_type,
|
||||
region_name=region_name,
|
||||
service_name=None,
|
||||
user_agent='python-cyborgclient')
|
||||
else:
|
||||
return HTTPClient(*args, **kwargs)
|
100
cyborgclient/common/utils.py
Normal file
100
cyborgclient/common/utils.py
Normal file
@ -0,0 +1,100 @@
|
||||
# Copyright 2016 Huawei, 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 logging
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def common_filters(marker=None, limit=None, sort_key=None, sort_dir=None):
|
||||
"""Generate common filters for any list request.
|
||||
|
||||
:param marker: entity ID from which to start returning entities.
|
||||
:param limit: maximum number of entities to return.
|
||||
:param sort_key: field to use for sorting.
|
||||
:param sort_dir: direction of sorting: 'asc' or 'desc'.
|
||||
:returns: list of string filters.
|
||||
"""
|
||||
filters = []
|
||||
if isinstance(limit, int):
|
||||
filters.append('limit=%s' % limit)
|
||||
if marker is not None:
|
||||
filters.append('marker=%s' % marker)
|
||||
if sort_key is not None:
|
||||
filters.append('sort_key=%s' % sort_key)
|
||||
if sort_dir is not None:
|
||||
filters.append('sort_dir=%s' % sort_dir)
|
||||
return filters
|
||||
|
||||
|
||||
def print_list_field(field):
|
||||
return lambda obj: ', '.join(getattr(obj, field))
|
||||
|
||||
|
||||
def get_response_body(resp):
|
||||
body = resp.content
|
||||
content_type = resp.headers.get('Content-Type', '')
|
||||
if 'application/json' in content_type:
|
||||
try:
|
||||
body = resp.json()
|
||||
except ValueError:
|
||||
LOG.error('Could not decode response body as JSON')
|
||||
elif 'application/octet-stream' in content_type:
|
||||
try:
|
||||
body = resp.body()
|
||||
except ValueError:
|
||||
LOG.error('Could not decode response body as raw')
|
||||
else:
|
||||
body = None
|
||||
return body
|
||||
|
||||
|
||||
def addresses_formatter(network_client, networks):
|
||||
output = []
|
||||
for (network, addresses) in networks.items():
|
||||
if not addresses:
|
||||
continue
|
||||
addrs = [addr['addr'] for addr in addresses]
|
||||
network_data = network_client.find_network(
|
||||
network, ignore_missing=False)
|
||||
net_ident = network_data.name or network_data.id
|
||||
addresses_csv = ', '.join(addrs)
|
||||
group = "%s=%s" % (net_ident, addresses_csv)
|
||||
output.append(group)
|
||||
return '; '.join(output)
|
||||
|
||||
|
||||
def image_formatter(image_client, image_id):
|
||||
if image_id:
|
||||
image = image_client.images.get(image_id)
|
||||
return '%s (%s)' % (image.name, image_id)
|
||||
return ''
|
||||
|
||||
|
||||
def flavor_formatter(bc_client, flavor_id):
|
||||
if flavor_id:
|
||||
flavor = bc_client.flavor.get(flavor_id)
|
||||
return '%s (%s)' % (flavor.name, flavor_id)
|
||||
return ''
|
||||
|
||||
|
||||
def clean_listing_columns(headers, columns, data_sample):
|
||||
col_headers = []
|
||||
cols = []
|
||||
for header, col in zip(headers, columns):
|
||||
if hasattr(data_sample, col):
|
||||
col_headers.append(header)
|
||||
cols.append(col)
|
||||
return tuple(col_headers), tuple(cols)
|
515
cyborgclient/exceptions.py
Normal file
515
cyborgclient/exceptions.py
Normal file
@ -0,0 +1,515 @@
|
||||
# Copyright 2016 Huawei, 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 inspect
|
||||
import sys
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
import six
|
||||
|
||||
from cyborgclient.i18n import _
|
||||
|
||||
|
||||
class ClientException(Exception):
|
||||
"""The base exception class for all exceptions this library raises."""
|
||||
def __init__(self, message=None):
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return self.message or self.__class__.__doc__
|
||||
|
||||
|
||||
class InvalidAttribute(ClientException):
|
||||
"""Invalid attribute on API client side."""
|
||||
pass
|
||||
|
||||
|
||||
class ValidationError(ClientException):
|
||||
"""Error in validation on API client side."""
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedVersion(ClientException):
|
||||
"""User is trying to use an unsupported version of the API."""
|
||||
pass
|
||||
|
||||
|
||||
class CommandError(ClientException):
|
||||
"""Error in CLI tool."""
|
||||
pass
|
||||
|
||||
|
||||
class AuthorizationFailure(ClientException):
|
||||
"""Cannot authorize API client."""
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionError(ClientException):
|
||||
"""Cannot connect to API service."""
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionRefused(ConnectionError):
|
||||
"""Connection refused while trying to connect to API service."""
|
||||
pass
|
||||
|
||||
|
||||
class AuthPluginOptionsMissing(AuthorizationFailure):
|
||||
"""Auth plugin misses some options."""
|
||||
def __init__(self, opt_names):
|
||||
super(AuthPluginOptionsMissing, self).__init__(
|
||||
_("Authentication failed. Missing options: %s") %
|
||||
", ".join(opt_names))
|
||||
self.opt_names = opt_names
|
||||
|
||||
|
||||
class AuthSystemNotFound(AuthorizationFailure):
|
||||
"""User has specified an AuthSystem that is not installed."""
|
||||
def __init__(self, auth_system):
|
||||
super(AuthSystemNotFound, self).__init__(
|
||||
_("AuthSystemNotFound: %r") % auth_system)
|
||||
self.auth_system = auth_system
|
||||
|
||||
|
||||
class NoUniqueMatch(ClientException):
|
||||
"""Multiple entities found instead of one."""
|
||||
pass
|
||||
|
||||
|
||||
class EndpointException(ClientException):
|
||||
"""Something is rotten in Service Catalog."""
|
||||
pass
|
||||
|
||||
|
||||
class EndpointNotFound(EndpointException):
|
||||
"""Could not find requested endpoint in Service Catalog."""
|
||||
pass
|
||||
|
||||
|
||||
class AmbiguousEndpoints(EndpointException):
|
||||
"""Found more than one matching endpoint in Service Catalog."""
|
||||
def __init__(self, endpoints=None):
|
||||
super(AmbiguousEndpoints, self).__init__(
|
||||
_("AmbiguousEndpoints: %r") % endpoints)
|
||||
self.endpoints = endpoints
|
||||
|
||||
|
||||
class HttpError(ClientException):
|
||||
"""The base exception class for all HTTP exceptions."""
|
||||
status_code = 0
|
||||
message = _("HTTP Error")
|
||||
|
||||
def __init__(self, message=None, details=None,
|
||||
response=None, request_id=None,
|
||||
url=None, method=None, status_code=None):
|
||||
self.status_code = status_code or self.status_code
|
||||
self.message = message or self.message
|
||||
self.details = details
|
||||
self.request_id = request_id
|
||||
self.response = response
|
||||
self.url = url
|
||||
self.method = method
|
||||
formatted_string = "%s (HTTP %s)" % (self.message, self.status_code)
|
||||
if request_id:
|
||||
formatted_string += " (Request-ID: %s)" % request_id
|
||||
super(HttpError, self).__init__(formatted_string)
|
||||
|
||||
|
||||
class HTTPRedirection(HttpError):
|
||||
"""HTTP Redirection."""
|
||||
message = _("HTTP Redirection")
|
||||
|
||||
|
||||
class HTTPClientError(HttpError):
|
||||
"""Client-side HTTP error.
|
||||
|
||||
Exception for cases in which the client seems to have erred.
|
||||
"""
|
||||
message = _("HTTP Client Error")
|
||||
|
||||
|
||||
class HttpServerError(HttpError):
|
||||
"""Server-side HTTP error.
|
||||
|
||||
Exception for cases in which the server is aware that it has
|
||||
erred or is incapable of performing the request.
|
||||
"""
|
||||
message = _("HTTP Server Error")
|
||||
|
||||
|
||||
class MultipleChoices(HTTPRedirection):
|
||||
"""HTTP 300 - Multiple Choices.
|
||||
|
||||
Indicates multiple options for the resource that the client may follow.
|
||||
"""
|
||||
|
||||
status_code = 300
|
||||
message = _("Multiple Choices")
|
||||
|
||||
|
||||
class BadRequest(HTTPClientError):
|
||||
"""HTTP 400 - Bad Request.
|
||||
|
||||
The request cannot be fulfilled due to bad syntax.
|
||||
"""
|
||||
status_code = 400
|
||||
message = _("Bad Request")
|
||||
|
||||
|
||||
class Unauthorized(HTTPClientError):
|
||||
"""HTTP 401 - Unauthorized.
|
||||
|
||||
Similar to 403 Forbidden, but specifically for use when authentication
|
||||
is required and has failed or has not yet been provided.
|
||||
"""
|
||||
status_code = 401
|
||||
message = _("Unauthorized")
|
||||
|
||||
|
||||
class PaymentRequired(HTTPClientError):
|
||||
"""HTTP 402 - Payment Required.
|
||||
|
||||
Reserved for future use.
|
||||
"""
|
||||
status_code = 402
|
||||
message = _("Payment Required")
|
||||
|
||||
|
||||
class Forbidden(HTTPClientError):
|
||||
"""HTTP 403 - Forbidden.
|
||||
|
||||
The request was a valid request, but the server is refusing to respond
|
||||
to it.
|
||||
"""
|
||||
status_code = 403
|
||||
message = _("Forbidden")
|
||||
|
||||
|
||||
class NotFound(HTTPClientError):
|
||||
"""HTTP 404 - Not Found.
|
||||
|
||||
The requested resource could not be found but may be available again
|
||||
in the future.
|
||||
"""
|
||||
status_code = 404
|
||||
message = _("Not Found")
|
||||
|
||||
|
||||
class MethodNotAllowed(HTTPClientError):
|
||||
"""HTTP 405 - Method Not Allowed.
|
||||
|
||||
A request was made of a resource using a request method not supported
|
||||
by that resource.
|
||||
"""
|
||||
status_code = 405
|
||||
message = _("Method Not Allowed")
|
||||
|
||||
|
||||
class NotAcceptable(HTTPClientError):
|
||||
"""HTTP 406 - Not Acceptable.
|
||||
|
||||
The requested resource is only capable of generating content not
|
||||
acceptable according to the Accept headers sent in the request.
|
||||
"""
|
||||
status_code = 406
|
||||
message = _("Not Acceptable")
|
||||
|
||||
|
||||
class ProxyAuthenticationRequired(HTTPClientError):
|
||||
"""HTTP 407 - Proxy Authentication Required.
|
||||
|
||||
The client must first authenticate itself with the proxy.
|
||||
"""
|
||||
status_code = 407
|
||||
message = _("Proxy Authentication Required")
|
||||
|
||||
|
||||
class RequestTimeout(HTTPClientError):
|
||||
"""HTTP 408 - Request Timeout.
|
||||
|
||||
The server timed out waiting for the request.
|
||||
"""
|
||||
status_code = 408
|
||||
message = _("Request Timeout")
|
||||
|
||||
|
||||
class Conflict(HTTPClientError):
|
||||
"""HTTP 409 - Conflict.
|
||||
|
||||
Indicates that the request could not be processed because of conflict
|
||||
in the request, such as an edit conflict.
|
||||
"""
|
||||
status_code = 409
|
||||
message = _("Conflict")
|
||||
|
||||
|
||||
class Gone(HTTPClientError):
|
||||
"""HTTP 410 - Gone.
|
||||
|
||||
Indicates that the resource requested is no longer available and will
|
||||
not be available again.
|
||||
"""
|
||||
status_code = 410
|
||||
message = _("Gone")
|
||||
|
||||
|
||||
class LengthRequired(HTTPClientError):
|
||||
"""HTTP 411 - Length Required.
|
||||
|
||||
The request did not specify the length of its content, which is
|
||||
required by the requested resource.
|
||||
"""
|
||||
status_code = 411
|
||||
message = _("Length Required")
|
||||
|
||||
|
||||
class PreconditionFailed(HTTPClientError):
|
||||
"""HTTP 412 - Precondition Failed.
|
||||
|
||||
The server does not meet one of the preconditions that the requester
|
||||
put on the request.
|
||||
"""
|
||||
status_code = 412
|
||||
message = _("Precondition Failed")
|
||||
|
||||
|
||||
class RequestEntityTooLarge(HTTPClientError):
|
||||
"""HTTP 413 - Request Entity Too Large.
|
||||
|
||||
The request is larger than the server is willing or able to process.
|
||||
"""
|
||||
status_code = 413
|
||||
message = _("Request Entity Too Large")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
try:
|
||||
self.retry_after = int(kwargs.pop('retry_after'))
|
||||
except (KeyError, ValueError):
|
||||
self.retry_after = 0
|
||||
|
||||
super(RequestEntityTooLarge, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class RequestUriTooLong(HTTPClientError):
|
||||
"""HTTP 414 - Request-URI Too Long.
|
||||
|
||||
The URI provided was too long for the server to process.
|
||||
"""
|
||||
status_code = 414
|
||||
message = _("Request-URI Too Long")
|
||||
|
||||
|
||||
class UnsupportedMediaType(HTTPClientError):
|
||||
"""HTTP 415 - Unsupported Media Type.
|
||||
|
||||
The request entity has a media type which the server or resource does
|
||||
not support.
|
||||
"""
|
||||
status_code = 415
|
||||
message = _("Unsupported Media Type")
|
||||
|
||||
|
||||
class RequestedRangeNotSatisfiable(HTTPClientError):
|
||||
"""HTTP 416 - Requested Range Not Satisfiable.
|
||||
|
||||
The client has asked for a portion of the file, but the server cannot
|
||||
supply that portion.
|
||||
"""
|
||||
status_code = 416
|
||||
message = _("Requested Range Not Satisfiable")
|
||||
|
||||
|
||||
class ExpectationFailed(HTTPClientError):
|
||||
"""HTTP 417 - Expectation Failed.
|
||||
|
||||
The server cannot meet the requirements of the Expect request-header field.
|
||||
"""
|
||||
status_code = 417
|
||||
message = _("Expectation Failed")
|
||||
|
||||
|
||||
class UnprocessableEntity(HTTPClientError):
|
||||
"""HTTP 422 - Unprocessable Entity.
|
||||
|
||||
The request was well-formed but was unable to be followed due to semantic
|
||||
errors.
|
||||
"""
|
||||
status_code = 422
|
||||
message = _("Unprocessable Entity")
|
||||
|
||||
|
||||
class InternalServerError(HttpServerError):
|
||||
"""HTTP 500 - Internal Server Error.
|
||||
|
||||
A generic error message, given when no more specific message is suitable.
|
||||
"""
|
||||
status_code = 500
|
||||
message = _("Internal Server Error")
|
||||
|
||||
|
||||
# NotImplemented is a python keyword.
|
||||
class HttpNotImplemented(HttpServerError):
|
||||
"""HTTP 501 - Not Implemented.
|
||||
|
||||
The server either does not recognize the request method, or it lacks
|
||||
the ability to fulfill the request.
|
||||
"""
|
||||
status_code = 501
|
||||
message = _("Not Implemented")
|
||||
|
||||
|
||||
class BadGateway(HttpServerError):
|
||||
"""HTTP 502 - Bad Gateway.
|
||||
|
||||
The server was acting as a gateway or proxy and received an invalid
|
||||
response from the upstream server.
|
||||
"""
|
||||
status_code = 502
|
||||
message = _("Bad Gateway")
|
||||
|
||||
|
||||
class ServiceUnavailable(HttpServerError):
|
||||
"""HTTP 503 - Service Unavailable.
|
||||
|
||||
The server is currently unavailable.
|
||||
"""
|
||||
status_code = 503
|
||||
message = _("Service Unavailable")
|
||||
|
||||
|
||||
class GatewayTimeout(HttpServerError):
|
||||
"""HTTP 504 - Gateway Timeout.
|
||||
|
||||
The server was acting as a gateway or proxy and did not receive a timely
|
||||
response from the upstream server.
|
||||
"""
|
||||
status_code = 504
|
||||
message = _("Gateway Timeout")
|
||||
|
||||
|
||||
class HttpVersionNotSupported(HttpServerError):
|
||||
"""HTTP 505 - HttpVersion Not Supported.
|
||||
|
||||
The server does not support the HTTP protocol version used in the request.
|
||||
"""
|
||||
status_code = 505
|
||||
message = _("HTTP Version Not Supported")
|
||||
|
||||
|
||||
# _code_map contains all the classes that have status_code attribute.
|
||||
_code_map = dict(
|
||||
(getattr(obj, 'status_code', None), obj)
|
||||
for name, obj in vars(sys.modules[__name__]).items()
|
||||
if inspect.isclass(obj) and getattr(obj, 'status_code', False)
|
||||
)
|
||||
|
||||
|
||||
def from_response(response, method, url, message=None, traceback=None):
|
||||
"""Returns an instance of :class:`HttpError` or subclass based on response.
|
||||
|
||||
:param response: instance of `requests.Response` class
|
||||
:param method: HTTP method used for request
|
||||
:param url: URL used for request
|
||||
"""
|
||||
error_body = {}
|
||||
if message:
|
||||
error_body['message'] = message
|
||||
if traceback:
|
||||
error_body['details'] = traceback
|
||||
|
||||
if hasattr(response, 'status') and not hasattr(response, 'status_code'):
|
||||
# NOTE(akurilin): These modifications around response object give
|
||||
# ability to get all necessary information in method `from_response`
|
||||
# from common code, which expecting response object from `requests`
|
||||
# library instead of object from `httplib/httplib2` library.
|
||||
response.status_code = response.status
|
||||
response.headers = {
|
||||
'Content-Type': response.getheader('content-type', "")}
|
||||
|
||||
if hasattr(response, 'status_code'):
|
||||
# NOTE(hongbin): This allows SessionClient to handle faultstring.
|
||||
response.json = lambda: {'error': error_body}
|
||||
|
||||
if (response.headers.get('Content-Type', '').startswith('text/') and
|
||||
not hasattr(response, 'text')):
|
||||
# NOTE(clif_h): There seems to be a case in the
|
||||
# common.apiclient.exceptions module where if the
|
||||
# content-type of the response is text/* then it expects
|
||||
# the response to have a 'text' attribute, but that
|
||||
# doesn't always seem to necessarily be the case.
|
||||
# This is to work around that problem.
|
||||
response.text = ''
|
||||
|
||||
# NOTE(liusheng): for pecan's response, the request_id is
|
||||
# "Openstack-Request-Id"
|
||||
req_id = (response.headers.get("x-openstack-request-id") or
|
||||
response.headers.get("Openstack-Request-Id"))
|
||||
kwargs = {
|
||||
"status_code": response.status_code,
|
||||
"response": response,
|
||||
"method": method,
|
||||
"url": url,
|
||||
"request_id": req_id,
|
||||
}
|
||||
if "retry-after" in response.headers:
|
||||
kwargs["retry_after"] = response.headers["retry-after"]
|
||||
|
||||
content_type = response.headers.get("Content-Type", "")
|
||||
if content_type.startswith("application/json"):
|
||||
try:
|
||||
body = response.json()
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
if hasattr(body, 'keys'):
|
||||
# NOTE(RuiChen): WebOb<1.6.0 will return a nested dict
|
||||
# structure where the error keys to the message/details/code.
|
||||
# WebOb>=1.6.0 returns just a response body as a single dict,
|
||||
# not nested, so we have to handle both cases (since we can't
|
||||
# trust what we're given with content_type: application/json
|
||||
# either way.
|
||||
if 'message' in body:
|
||||
# WebOb>=1.6.0 case
|
||||
error = body
|
||||
else:
|
||||
# WebOb<1.6.0 where we assume there is a single error
|
||||
# message key to the body that has the message and details.
|
||||
error = body.get(list(body)[0])
|
||||
# NOTE(liusheng): the response.json() may like this:
|
||||
# {u'error_message': u'{"debuginfo": null, "faultcode":
|
||||
# "Client", "faultstring": "error message"}'}, the
|
||||
# "error_message" in the body is also a json string.
|
||||
if isinstance(error, six.string_types):
|
||||
error = jsonutils.loads(error)
|
||||
|
||||
if hasattr(error, 'keys'):
|
||||
kwargs['message'] = (error.get('message') or
|
||||
error.get('faultstring'))
|
||||
kwargs['details'] = (error.get('details') or
|
||||
six.text_type(body))
|
||||
elif content_type.startswith("text/"):
|
||||
kwargs["details"] = getattr(response, 'text', '')
|
||||
|
||||
try:
|
||||
cls = _code_map[response.status_code]
|
||||
except KeyError:
|
||||
if 500 <= response.status_code < 600:
|
||||
cls = HttpServerError
|
||||
elif 400 <= response.status_code < 500:
|
||||
cls = HTTPClientError
|
||||
else:
|
||||
cls = HttpError
|
||||
|
||||
return cls(**kwargs)
|
19
cyborgclient/i18n.py
Normal file
19
cyborgclient/i18n.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Copyright 2016 Huawei, 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 oslo_i18n
|
||||
|
||||
# The primary translation function using the well-known name "_"
|
||||
_ = oslo_i18n.TranslatorFactory(domain='cyborgclient').primary
|
0
cyborgclient/osc/__init__.py
Normal file
0
cyborgclient/osc/__init__.py
Normal file
73
cyborgclient/osc/plugin.py
Normal file
73
cyborgclient/osc/plugin.py
Normal file
@ -0,0 +1,73 @@
|
||||
# Copyright 2016 Huawei, 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 logging
|
||||
|
||||
from osc_lib import utils
|
||||
|
||||
from cyborgclient.i18n import _
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_ACCELERATOR_API_VERSION = '1'
|
||||
API_VERSION_OPTION = 'os_accelerator_api_version'
|
||||
API_NAME = 'accelerator'
|
||||
API_VERSIONS = {
|
||||
'1': 'cyborgclient.v1.client.Client',
|
||||
}
|
||||
|
||||
|
||||
def make_client(instance):
|
||||
"""Returns an accelerators service client"""
|
||||
cyborg_client = utils.get_client_class(
|
||||
API_NAME,
|
||||
instance._api_version[API_NAME],
|
||||
API_VERSIONS)
|
||||
LOG.debug('Instantiating accelerators client: %s', cyborg_client)
|
||||
|
||||
endpoint = instance.get_endpoint_for_service_type(
|
||||
API_NAME,
|
||||
region_name=instance.region_name,
|
||||
interface=instance.interface,
|
||||
)
|
||||
|
||||
kwargs = {'endpoint': endpoint,
|
||||
'auth_url': instance.auth.auth_url,
|
||||
'region_name': instance.region_name,
|
||||
'username': instance.auth_ref.username}
|
||||
|
||||
if instance.session:
|
||||
kwargs.update(session=instance.session)
|
||||
else:
|
||||
kwargs.update(token=instance.auth_ref.auth_token)
|
||||
|
||||
client = cyborg_client(**kwargs)
|
||||
|
||||
return client
|
||||
|
||||
|
||||
def build_option_parser(parser):
|
||||
"""Hook to add global options"""
|
||||
parser.add_argument(
|
||||
'--os-accelerator-api-version',
|
||||
metavar='<accelerator-api-version>',
|
||||
default=utils.env(
|
||||
'OS_ACCELERATOR_API_VERSION',
|
||||
default=DEFAULT_ACCELERATOR_API_VERSION),
|
||||
help=(_('Accelerations compute API version, default=%s '
|
||||
'(Env: OS_ACCELERATOR_API_VERSION)') %
|
||||
DEFAULT_ACCELERATOR_API_VERSION)
|
||||
)
|
||||
return parser
|
0
cyborgclient/osc/v1/__init__.py
Normal file
0
cyborgclient/osc/v1/__init__.py
Normal file
69
cyborgclient/osc/v1/accelerator.py
Normal file
69
cyborgclient/osc/v1/accelerator.py
Normal file
@ -0,0 +1,69 @@
|
||||
# Copyright (c) 2018 Intel, 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.
|
||||
#
|
||||
|
||||
|
||||
"""Cyborg v1 Acceleration accelerator action implementations"""
|
||||
|
||||
|
||||
from osc_lib.command import command
|
||||
from osc_lib import utils
|
||||
|
||||
from cyborgclient.common import utils as cli_utils
|
||||
from cyborgclient.i18n import _
|
||||
|
||||
|
||||
class ListAccelerator(command.Lister):
|
||||
"""List all accelerators"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ListAccelerator, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'--long',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help=_("List additional fields in output")
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
acc_client = self.app.client_manager.accelerator
|
||||
|
||||
column_headers = (
|
||||
"UUID",
|
||||
"Name",
|
||||
"Type",
|
||||
"Description",
|
||||
"Device Type",
|
||||
"Capability",
|
||||
)
|
||||
columns = (
|
||||
"uuid",
|
||||
"name",
|
||||
"type",
|
||||
"description",
|
||||
"device_type",
|
||||
"capability",
|
||||
)
|
||||
|
||||
data = acc_client.accelerators.list()
|
||||
if not data:
|
||||
return (), ()
|
||||
column_headers, columns = cli_utils.clean_listing_columns(
|
||||
column_headers, columns, data[0])
|
||||
formatters = {}
|
||||
|
||||
return (column_headers,
|
||||
(utils.get_item_properties(
|
||||
s, columns, formatters=formatters) for s in data))
|
648
cyborgclient/shell.py
Normal file
648
cyborgclient/shell.py
Normal file
@ -0,0 +1,648 @@
|
||||
# Copyright 2014
|
||||
# The Cloudscaling Group, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
###
|
||||
# This code is taken from python-novaclient. Goal is minimal modification.
|
||||
###
|
||||
|
||||
"""
|
||||
Command-line interface to the OpenStack Cyborg API.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import importutils
|
||||
from oslo_utils import strutils
|
||||
import six
|
||||
|
||||
profiler = importutils.try_import("osprofiler.profiler")
|
||||
|
||||
HAS_KEYRING = False
|
||||
all_errors = ValueError
|
||||
try:
|
||||
import keyring
|
||||
HAS_KEYRING = True
|
||||
try:
|
||||
if isinstance(keyring.get_keyring(), keyring.backend.GnomeKeyring):
|
||||
import gnomekeyring
|
||||
all_errors = (ValueError,
|
||||
gnomekeyring.IOError,
|
||||
gnomekeyring.NoKeyringDaemonError)
|
||||
except Exception:
|
||||
pass
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from cyborgclient.common import cliutils
|
||||
from cyborgclient import exceptions as exc
|
||||
from cyborgclient.i18n import _
|
||||
from cyborgclient.v1 import client as client_v1
|
||||
from cyborgclient.v1 import shell as shell_v1
|
||||
from cyborgclient import version
|
||||
|
||||
LATEST_API_VERSION = ('1', 'latest')
|
||||
DEFAULT_INTERFACE = 'public'
|
||||
DEFAULT_SERVICE_TYPE = 'acceleration'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def positive_non_zero_float(text):
|
||||
if text is None:
|
||||
return None
|
||||
try:
|
||||
value = float(text)
|
||||
except ValueError:
|
||||
msg = "%s must be a float" % text
|
||||
raise argparse.ArgumentTypeError(msg)
|
||||
if value <= 0:
|
||||
msg = "%s must be greater than 0" % text
|
||||
raise argparse.ArgumentTypeError(msg)
|
||||
return value
|
||||
|
||||
|
||||
class CyborgClientArgumentParser(argparse.ArgumentParser):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CyborgClientArgumentParser, self).__init__(*args, **kwargs)
|
||||
|
||||
def error(self, message):
|
||||
"""error(message: string)
|
||||
|
||||
Prints a usage message incorporating the message to stderr and
|
||||
exits.
|
||||
"""
|
||||
self.print_usage(sys.stderr)
|
||||
# FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value
|
||||
choose_from = ' (choose from'
|
||||
progparts = self.prog.partition(' ')
|
||||
self.exit(2, "error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'"
|
||||
" for more information.\n" %
|
||||
{'errmsg': message.split(choose_from)[0],
|
||||
'mainp': progparts[0],
|
||||
'subp': progparts[2]})
|
||||
|
||||
|
||||
class OpenStackCyborgShell(object):
|
||||
|
||||
def get_base_parser(self):
|
||||
parser = CyborgClientArgumentParser(
|
||||
prog='cyborg',
|
||||
description=__doc__.strip(),
|
||||
epilog='See "cyborg help COMMAND" '
|
||||
'for help on a specific command.',
|
||||
add_help=False,
|
||||
formatter_class=OpenStackHelpFormatter,
|
||||
)
|
||||
|
||||
# Global arguments
|
||||
parser.add_argument('-h', '--help',
|
||||
action='store_true',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--version',
|
||||
action='version',
|
||||
version=version.version_info.version_string())
|
||||
|
||||
parser.add_argument('--debug',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help=_("Print debugging output."))
|
||||
|
||||
parser.add_argument('--os-cache',
|
||||
default=strutils.bool_from_string(
|
||||
cliutils.env('OS_CACHE', default=False)),
|
||||
action='store_true',
|
||||
help=_("Use the auth token cache. Defaults to "
|
||||
"False if env[OS_CACHE] is not set."))
|
||||
|
||||
parser.add_argument('--os-region-name',
|
||||
metavar='<region-name>',
|
||||
default=os.environ.get('OS_REGION_NAME'),
|
||||
help=_('Region name. '
|
||||
'Default=env[OS_REGION_NAME].'))
|
||||
|
||||
# TODO(mattf) - add get_timings support to Client
|
||||
# parser.add_argument('--timings',
|
||||
# default=False,
|
||||
# action='store_true',
|
||||
# help="Print call timing info")
|
||||
|
||||
# TODO(mattf) - use timeout
|
||||
# parser.add_argument('--timeout',
|
||||
# default=600,
|
||||
# metavar='<seconds>',
|
||||
# type=positive_non_zero_float,
|
||||
# help="Set HTTP call timeout (in seconds)")
|
||||
|
||||
parser.add_argument('--os-auth-url',
|
||||
metavar='<auth-auth-url>',
|
||||
default=cliutils.env('OS_AUTH_URL', default=None),
|
||||
help=_('Defaults to env[OS_AUTH_URL].'))
|
||||
|
||||
parser.add_argument('--os-user-id',
|
||||
metavar='<auth-user-id>',
|
||||
default=cliutils.env('OS_USER_ID', default=None),
|
||||
help=_('Defaults to env[OS_USER_ID].'))
|
||||
|
||||
parser.add_argument('--os-username',
|
||||
metavar='<auth-username>',
|
||||
default=cliutils.env('OS_USERNAME', default=None),
|
||||
help=_('Defaults to env[OS_USERNAME].'))
|
||||
|
||||
parser.add_argument('--os-user-domain-id',
|
||||
metavar='<auth-user-domain-id>',
|
||||
default=cliutils.env('OS_USER_DOMAIN_ID',
|
||||
default=None),
|
||||
help=_('Defaults to env[OS_USER_DOMAIN_ID].'))
|
||||
|
||||
parser.add_argument('--os-user-domain-name',
|
||||
metavar='<auth-user-domain-name>',
|
||||
default=cliutils.env('OS_USER_DOMAIN_NAME',
|
||||
default=None),
|
||||
help=_('Defaults to env[OS_USER_DOMAIN_NAME].'))
|
||||
|
||||
parser.add_argument('--os-project-id',
|
||||
metavar='<auth-project-id>',
|
||||
default=cliutils.env('OS_PROJECT_ID',
|
||||
default=None),
|
||||
help=_('Defaults to env[OS_PROJECT_ID].'))
|
||||
|
||||
parser.add_argument('--os-project-name',
|
||||
metavar='<auth-project-name>',
|
||||
default=cliutils.env('OS_PROJECT_NAME',
|
||||
default=None),
|
||||
help=_('Defaults to env[OS_PROJECT_NAME].'))
|
||||
|
||||
parser.add_argument('--os-tenant-id',
|
||||
metavar='<auth-tenant-id>',
|
||||
default=cliutils.env('OS_TENANT_ID',
|
||||
default=None),
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-tenant-name',
|
||||
metavar='<auth-tenant-name>',
|
||||
default=cliutils.env('OS_TENANT_NAME',
|
||||
default=None),
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-project-domain-id',
|
||||
metavar='<auth-project-domain-id>',
|
||||
default=cliutils.env('OS_PROJECT_DOMAIN_ID',
|
||||
default=None),
|
||||
help=_('Defaults to env[OS_PROJECT_DOMAIN_ID].'))
|
||||
|
||||
parser.add_argument('--os-project-domain-name',
|
||||
metavar='<auth-project-domain-name>',
|
||||
default=cliutils.env('OS_PROJECT_DOMAIN_NAME',
|
||||
default=None),
|
||||
help=_('Defaults to '
|
||||
'env[OS_PROJECT_DOMAIN_NAME].'))
|
||||
|
||||
parser.add_argument('--os-token',
|
||||
metavar='<auth-token>',
|
||||
default=cliutils.env('OS_TOKEN', default=None),
|
||||
help=_('Defaults to env[OS_TOKEN].'))
|
||||
|
||||
parser.add_argument('--os-password',
|
||||
metavar='<auth-password>',
|
||||
default=cliutils.env('OS_PASSWORD',
|
||||
default=None),
|
||||
help=_('Defaults to env[OS_PASSWORD].'))
|
||||
|
||||
parser.add_argument('--service-type',
|
||||
metavar='<service-type>',
|
||||
help=_('Defaults to container-infra for all '
|
||||
'actions.'))
|
||||
parser.add_argument('--service_type',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--endpoint-type',
|
||||
metavar='<endpoint-type>',
|
||||
default=cliutils.env('OS_ENDPOINT_TYPE',
|
||||
default=None),
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-endpoint-type',
|
||||
metavar='<os-endpoint-type>',
|
||||
default=cliutils.env('OS_ENDPOINT_TYPE',
|
||||
default=None),
|
||||
help=_('Defaults to env[OS_ENDPOINT_TYPE]'))
|
||||
|
||||
parser.add_argument('--os-interface',
|
||||
metavar='<os-interface>',
|
||||
default=cliutils.env(
|
||||
'OS_INTERFACE',
|
||||
default=DEFAULT_INTERFACE),
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-cloud',
|
||||
metavar='<auth-cloud>',
|
||||
default=cliutils.env('OS_CLOUD', default=None),
|
||||
help=_('Defaults to env[OS_CLOUD].'))
|
||||
|
||||
# NOTE(dtroyer): We can't add --endpoint_type here due to argparse
|
||||
# thinking usage-list --end is ambiguous; but it
|
||||
# works fine with only --endpoint-type present
|
||||
# Go figure. I'm leaving this here for doc purposes.
|
||||
# parser.add_argument('--endpoint_type',
|
||||
# help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--cyborg-api-version',
|
||||
metavar='<cyborg-api-ver>',
|
||||
default=cliutils.env(
|
||||
'CYBORG_API_VERSION',
|
||||
default='latest'),
|
||||
help=_('Accepts "api", '
|
||||
'defaults to env[CYBORG_API_VERSION].'))
|
||||
parser.add_argument('--cyborg_api_version',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-cacert',
|
||||
metavar='<ca-certificate>',
|
||||
default=cliutils.env('OS_CACERT', default=None),
|
||||
help=_('Specify a CA bundle file to use in '
|
||||
'verifying a TLS (https) server '
|
||||
'certificate. Defaults to env[OS_CACERT].'))
|
||||
|
||||
parser.add_argument('--os-endpoint-override',
|
||||
metavar='<endpoint-override>',
|
||||
default=cliutils.env('OS_ENDPOINT_OVERRIDE',
|
||||
default=None),
|
||||
help=_("Use this API endpoint instead of the "
|
||||
"Service Catalog."))
|
||||
parser.add_argument('--bypass-url',
|
||||
metavar='<bypass-url>',
|
||||
default=cliutils.env('BYPASS_URL', default=None),
|
||||
dest='bypass_url',
|
||||
help=argparse.SUPPRESS)
|
||||
parser.add_argument('--bypass_url',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--insecure',
|
||||
default=cliutils.env('CYBORGCLIENT_INSECURE',
|
||||
default=False),
|
||||
action='store_true',
|
||||
help=_("Do not verify https connections"))
|
||||
|
||||
if profiler:
|
||||
parser.add_argument('--profile',
|
||||
metavar='HMAC_KEY',
|
||||
default=cliutils.env('OS_PROFILE',
|
||||
default=None),
|
||||
help='HMAC key to use for encrypting context '
|
||||
'data for performance profiling of operation. '
|
||||
'This key should be the value of the HMAC key '
|
||||
'configured for the OSprofiler middleware in '
|
||||
'cyborg; it is specified in the Cyborg '
|
||||
'configuration file at '
|
||||
'"/etc/cyborg/magnum.conf". '
|
||||
'Without the key, profiling will not be '
|
||||
'triggered even if OSprofiler is enabled on '
|
||||
'the server side.')
|
||||
|
||||
return parser
|
||||
|
||||
def get_subcommand_parser(self, version):
|
||||
parser = self.get_base_parser()
|
||||
|
||||
self.subcommands = {}
|
||||
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||
|
||||
try:
|
||||
actions_modules = {
|
||||
'1': shell_v1.COMMAND_MODULES
|
||||
}[version]
|
||||
except KeyError:
|
||||
actions_modules = shell_v1.COMMAND_MODULES
|
||||
|
||||
for actions_module in actions_modules:
|
||||
self._find_actions(subparsers, actions_module)
|
||||
self._find_actions(subparsers, self)
|
||||
|
||||
self._add_bash_completion_subparser(subparsers)
|
||||
|
||||
return parser
|
||||
|
||||
def _add_bash_completion_subparser(self, subparsers):
|
||||
subparser = (
|
||||
subparsers.add_parser('bash_completion',
|
||||
add_help=False,
|
||||
formatter_class=OpenStackHelpFormatter)
|
||||
)
|
||||
self.subcommands['bash_completion'] = subparser
|
||||
subparser.set_defaults(func=self.do_bash_completion)
|
||||
|
||||
def _find_actions(self, subparsers, actions_module):
|
||||
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
|
||||
# I prefer to be hyphen-separated instead of underscores.
|
||||
command = attr[3:].replace('_', '-')
|
||||
callback = getattr(actions_module, attr)
|
||||
desc = callback.__doc__ or ''
|
||||
action_help = desc.strip()
|
||||
arguments = getattr(callback, 'arguments', [])
|
||||
group_args = getattr(callback, 'deprecated_groups', [])
|
||||
|
||||
subparser = (
|
||||
subparsers.add_parser(command,
|
||||
help=action_help,
|
||||
description=desc,
|
||||
add_help=False,
|
||||
formatter_class=OpenStackHelpFormatter)
|
||||
)
|
||||
subparser.add_argument('-h', '--help',
|
||||
action='help',
|
||||
help=argparse.SUPPRESS,)
|
||||
self.subcommands[command] = subparser
|
||||
|
||||
for (old_info, new_info, req) in group_args:
|
||||
group = subparser.add_mutually_exclusive_group(required=req)
|
||||
group.add_argument(*old_info[0], **old_info[1])
|
||||
group.add_argument(*new_info[0], **new_info[1])
|
||||
|
||||
for (args, kwargs) in arguments:
|
||||
subparser.add_argument(*args, **kwargs)
|
||||
subparser.set_defaults(func=callback)
|
||||
|
||||
def setup_debugging(self, debug):
|
||||
if debug:
|
||||
streamformat = "%(levelname)s (%(module)s:%(lineno)d) %(message)s"
|
||||
# Set up the root logger to debug so that the submodules can
|
||||
# print debug messages
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format=streamformat)
|
||||
else:
|
||||
streamformat = "%(levelname)s %(message)s"
|
||||
logging.basicConfig(level=logging.CRITICAL,
|
||||
format=streamformat)
|
||||
|
||||
def _check_version(self, api_version):
|
||||
if api_version == 'latest':
|
||||
return LATEST_API_VERSION
|
||||
else:
|
||||
try:
|
||||
versions = tuple(int(i) for i in api_version.split('.'))
|
||||
except ValueError:
|
||||
versions = ()
|
||||
if len(versions) == 1:
|
||||
# Default value of cyborg_api_version is '1'.
|
||||
# If user not specify the value of api version, not passing
|
||||
# headers at all.
|
||||
cyborg_api_version = None
|
||||
elif len(versions) == 2:
|
||||
cyborg_api_version = api_version
|
||||
# In the case of '1.0'
|
||||
if versions[1] == 0:
|
||||
cyborg_api_version = None
|
||||
else:
|
||||
msg = _("The requested API version %(ver)s is an unexpected "
|
||||
"format. Acceptable formats are 'X', 'X.Y', or the "
|
||||
"literal string '%(latest)s'."
|
||||
) % {'ver': api_version, 'latest': 'latest'}
|
||||
raise exc.CommandError(msg)
|
||||
|
||||
api_major_version = versions[0]
|
||||
return (api_major_version, cyborg_api_version)
|
||||
|
||||
def _ensure_auth_info(self, args):
|
||||
if not cliutils.isunauthenticated(args.func):
|
||||
if (not (args.os_token and
|
||||
(args.os_auth_url or args.os_endpoint_override)) and
|
||||
not args.os_cloud):
|
||||
|
||||
if not (args.os_username or args.os_user_id):
|
||||
raise exc.CommandError(
|
||||
"You must provide a username via either --os-username "
|
||||
"or via env[OS_USERNAME]"
|
||||
)
|
||||
if not args.os_password:
|
||||
raise exc.CommandError(
|
||||
"You must provide a password via either "
|
||||
"--os-password, env[OS_PASSWORD], or prompted "
|
||||
"response"
|
||||
)
|
||||
if (not args.os_project_name and not args.os_project_id):
|
||||
raise exc.CommandError(
|
||||
"You must provide a project name or project id via "
|
||||
"--os-project-name, --os-project-id, "
|
||||
"env[OS_PROJECT_NAME] or env[OS_PROJECT_ID]"
|
||||
)
|
||||
if not args.os_auth_url:
|
||||
raise exc.CommandError(
|
||||
"You must provide an auth url via either "
|
||||
"--os-auth-url or via env[OS_AUTH_URL]"
|
||||
)
|
||||
|
||||
def main(self, argv):
|
||||
|
||||
# NOTE(Christoph Jansen): With Python 3.4 argv somehow becomes a Map.
|
||||
# This hack fixes it.
|
||||
argv = list(argv)
|
||||
|
||||
# Parse args once to find version and debug settings
|
||||
parser = self.get_base_parser()
|
||||
(options, args) = parser.parse_known_args(argv)
|
||||
self.setup_debugging(options.debug)
|
||||
|
||||
# NOTE(dtroyer): Hackery to handle --endpoint_type due to argparse
|
||||
# thinking usage-list --end is ambiguous; but it
|
||||
# works fine with only --endpoint-type present
|
||||
# Go figure.
|
||||
if '--endpoint_type' in argv:
|
||||
spot = argv.index('--endpoint_type')
|
||||
argv[spot] = '--endpoint-type'
|
||||
|
||||
# build available subcommands based on version
|
||||
(api_major_version, cyborg_api_version) = (
|
||||
self._check_version(options.cyborg_api_version))
|
||||
|
||||
subcommand_parser = (
|
||||
self.get_subcommand_parser(api_major_version)
|
||||
)
|
||||
self.parser = subcommand_parser
|
||||
|
||||
if options.help or not argv:
|
||||
subcommand_parser.print_help()
|
||||
return 0
|
||||
|
||||
args = subcommand_parser.parse_args(argv)
|
||||
|
||||
# Short-circuit and deal with help right away.
|
||||
# NOTE(jamespage): args.func is not guaranteed with python >= 3.4
|
||||
if not hasattr(args, 'func') or args.func == self.do_help:
|
||||
self.do_help(args)
|
||||
return 0
|
||||
elif args.func == self.do_bash_completion:
|
||||
self.do_bash_completion(args)
|
||||
return 0
|
||||
|
||||
if not args.service_type:
|
||||
args.service_type = DEFAULT_SERVICE_TYPE
|
||||
|
||||
if args.bypass_url:
|
||||
args.os_endpoint_override = args.bypass_url
|
||||
|
||||
args.os_project_id = (args.os_project_id or args.os_tenant_id)
|
||||
args.os_project_name = (args.os_project_name or args.os_tenant_name)
|
||||
|
||||
self._ensure_auth_info(args)
|
||||
|
||||
try:
|
||||
client = {
|
||||
'1': client_v1,
|
||||
}[api_major_version]
|
||||
except KeyError:
|
||||
client = client_v1
|
||||
|
||||
args.os_endpoint_type = (args.os_endpoint_type or args.endpoint_type)
|
||||
if args.os_endpoint_type:
|
||||
args.os_interface = args.os_endpoint_type
|
||||
|
||||
if args.os_interface.endswith('URL'):
|
||||
args.os_interface = args.os_interface[:-3]
|
||||
|
||||
kwargs = {}
|
||||
if profiler:
|
||||
kwargs["profile"] = args.profile
|
||||
|
||||
self.cs = client.Client(
|
||||
cloud=args.os_cloud,
|
||||
user_id=args.os_user_id,
|
||||
username=args.os_username,
|
||||
password=args.os_password,
|
||||
auth_token=args.os_token,
|
||||
project_id=args.os_project_id,
|
||||
project_name=args.os_project_name,
|
||||
user_domain_id=args.os_user_domain_id,
|
||||
user_domain_name=args.os_user_domain_name,
|
||||
project_domain_id=args.os_project_domain_id,
|
||||
project_domain_name=args.os_project_domain_name,
|
||||
auth_url=args.os_auth_url,
|
||||
service_type=args.service_type,
|
||||
region_name=args.os_region_name,
|
||||
cyborg_url=args.os_endpoint_override,
|
||||
interface=args.os_interface,
|
||||
insecure=args.insecure,
|
||||
api_version=args.cyborg_api_version,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
self._check_deprecation(args.func, argv)
|
||||
try:
|
||||
args.func(self.cs, args)
|
||||
except (cliutils.DuplicateArgs, cliutils.MissingArgs):
|
||||
self.do_help(args)
|
||||
raise
|
||||
|
||||
if profiler and args.profile:
|
||||
trace_id = profiler.get().get_base_id()
|
||||
print("To display trace use the command:\n\n"
|
||||
" osprofiler trace show --html %s " % trace_id)
|
||||
|
||||
def _check_deprecation(self, func, argv):
|
||||
if not hasattr(func, 'deprecated_groups'):
|
||||
return
|
||||
|
||||
for (old_info, new_info, required) in func.deprecated_groups:
|
||||
old_param = old_info[0][0]
|
||||
new_param = new_info[0][0]
|
||||
old_value, new_value = None, None
|
||||
for i in range(len(argv)):
|
||||
cur_arg = argv[i]
|
||||
if cur_arg == old_param:
|
||||
old_value = argv[i + 1]
|
||||
elif cur_arg == new_param[0]:
|
||||
new_value = argv[i + 1]
|
||||
|
||||
if old_value and not new_value:
|
||||
print(
|
||||
'WARNING: The %s parameter is deprecated and will be '
|
||||
'removed in a future release. Use the %s parameter to '
|
||||
'avoid seeing this message.'
|
||||
% (old_param, new_param))
|
||||
|
||||
def _dump_timings(self, timings):
|
||||
class Tyme(object):
|
||||
def __init__(self, url, seconds):
|
||||
self.url = url
|
||||
self.seconds = seconds
|
||||
results = [Tyme(url, end - start) for url, start, end in timings]
|
||||
total = 0.0
|
||||
for tyme in results:
|
||||
total += tyme.seconds
|
||||
results.append(Tyme("Total", total))
|
||||
cliutils.print_list(results, ["url", "seconds"], sortby_index=None)
|
||||
|
||||
def do_bash_completion(self, _args):
|
||||
"""Prints arguments for bash-completion.
|
||||
|
||||
Prints all of the commands and options to stdout so that the
|
||||
cyborg.bash_completion script doesn't have to hard code them.
|
||||
"""
|
||||
commands = set()
|
||||
options = set()
|
||||
for sc_str, sc in self.subcommands.items():
|
||||
commands.add(sc_str)
|
||||
for option in sc._optionals._option_string_actions.keys():
|
||||
options.add(option)
|
||||
|
||||
commands.remove('bash-completion')
|
||||
commands.remove('bash_completion')
|
||||
print(' '.join(commands | options))
|
||||
|
||||
@cliutils.arg('command', metavar='<subcommand>', nargs='?',
|
||||
help=_('Display help for <subcommand>.'))
|
||||
def do_help(self, args):
|
||||
"""Display help about this program or one of its subcommands."""
|
||||
# NOTE(jamespage): args.command is not guaranteed with python >= 3.4
|
||||
command = getattr(args, 'command', '')
|
||||
|
||||
if command:
|
||||
if args.command in self.subcommands:
|
||||
self.subcommands[args.command].print_help()
|
||||
else:
|
||||
raise exc.CommandError("'%s' is not a valid subcommand" %
|
||||
args.command)
|
||||
else:
|
||||
self.parser.print_help()
|
||||
|
||||
|
||||
# I'm picky about my shell help.
|
||||
class OpenStackHelpFormatter(argparse.HelpFormatter):
|
||||
def start_section(self, heading):
|
||||
# Title-case the headings
|
||||
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
||||
super(OpenStackHelpFormatter, self).start_section(heading)
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
OpenStackCyborgShell().main(map(encodeutils.safe_decode, sys.argv[1:]))
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(e, exc_info=1)
|
||||
print("ERROR: %s" % encodeutils.safe_encode(six.text_type(e)),
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
0
cyborgclient/v1/__init__.py
Normal file
0
cyborgclient/v1/__init__.py
Normal file
27
cyborgclient/v1/accelerators.py
Normal file
27
cyborgclient/v1/accelerators.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Copyright (c) 2018 Intel, 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.
|
||||
|
||||
from cyborgclient.v1 import basemodels
|
||||
|
||||
|
||||
CREATION_ATTRIBUTES = basemodels.CREATION_ATTRIBUTES
|
||||
|
||||
|
||||
class Accelerator(basemodels.BaseModel):
|
||||
model_name = "Accelerator"
|
||||
|
||||
|
||||
class AcceleratorManager(basemodels.BaseModelManager):
|
||||
api_name = "accelerators"
|
||||
resource_class = Accelerator
|
61
cyborgclient/v1/accelerators_shell.py
Normal file
61
cyborgclient/v1/accelerators_shell.py
Normal file
@ -0,0 +1,61 @@
|
||||
# Copyright (c) 2018 Intel, 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.
|
||||
|
||||
from cyborgclient.common import cliutils as utils
|
||||
from cyborgclient.common import utils as cyborg_utils
|
||||
from cyborgclient.i18n import _
|
||||
from cyborgclient.v1 import basemodels
|
||||
|
||||
|
||||
@utils.arg('--limit',
|
||||
metavar='<limit>',
|
||||
type=int,
|
||||
help=_('Maximum number of accelerators to return'))
|
||||
@utils.arg('--sort-key',
|
||||
metavar='<sort-key>',
|
||||
help=_('Column to sort results by'))
|
||||
@utils.arg('--sort-dir',
|
||||
metavar='<sort-dir>',
|
||||
choices=['desc', 'asc'],
|
||||
help=_('Direction to sort. "asc" or "desc".'))
|
||||
@utils.arg('--fields',
|
||||
default=None,
|
||||
metavar='<fields>',
|
||||
help=_('Comma-separated list of fields to display. '
|
||||
'Available fields: uuid, name'
|
||||
)
|
||||
)
|
||||
@utils.arg('--detail',
|
||||
action='store_true', default=False,
|
||||
help=_('Show detailed information about the accelerators.')
|
||||
)
|
||||
def do_accelerator_list(cs, args):
|
||||
"""Print a list of accelerators.
|
||||
|
||||
(Deprecated in favor of cluster-template-list.)
|
||||
"""
|
||||
accelerators = cs.accelerators.list(limit=args.limit,
|
||||
sort_key=args.sort_key,
|
||||
sort_dir=args.sort_dir,
|
||||
detail=args.detail)
|
||||
if args.detail:
|
||||
columns = basemodels.OUTPUT_ATTRIBUTES
|
||||
else:
|
||||
columns = ['uuid', 'name']
|
||||
columns += utils._get_list_table_columns_and_formatters(
|
||||
args.fields, accelerators,
|
||||
exclude_fields=(c.lower() for c in columns))[0]
|
||||
utils.print_list(accelerators, columns,
|
||||
{'versions': cyborg_utils.print_list_field('versions')},
|
||||
sortby_index=None)
|
111
cyborgclient/v1/basemodels.py
Normal file
111
cyborgclient/v1/basemodels.py
Normal file
@ -0,0 +1,111 @@
|
||||
# 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.
|
||||
|
||||
from cyborgclient.common import base
|
||||
from cyborgclient.common import utils
|
||||
from cyborgclient import exceptions
|
||||
|
||||
|
||||
CREATION_ATTRIBUTES = []
|
||||
|
||||
OUTPUT_ATTRIBUTES = CREATION_ATTRIBUTES + ['apiserver_port', 'created_at',
|
||||
'insecure_registry', 'links',
|
||||
'updated_at', 'cluster_distro',
|
||||
'uuid']
|
||||
|
||||
|
||||
class BaseModel(base.Resource):
|
||||
# model_name needs to be overridden by any derived class.
|
||||
# model_name should be capitalized and singular, e.g. "Cluster"
|
||||
model_name = ''
|
||||
|
||||
def __repr__(self):
|
||||
return "<" + self.__class__.model_name + "%s>" % self._info
|
||||
|
||||
|
||||
class BaseModelManager(base.Manager):
|
||||
# api_name needs to be overridden by any derived class.
|
||||
# api_name should be pluralized and lowercase, e.g. "clustertemplates", as
|
||||
# it shows up in the URL path: "/v1/{api_name}"
|
||||
api_name = ''
|
||||
|
||||
@classmethod
|
||||
def _path(cls, id=None):
|
||||
return '/v1/' + cls.api_name + \
|
||||
'/%s' % id if id else '/v1/' + cls.api_name
|
||||
|
||||
def list(self, limit=None, marker=None, sort_key=None,
|
||||
sort_dir=None, detail=False):
|
||||
"""Retrieve a list of accelerators.
|
||||
|
||||
:param marker: Optional, the UUID of a baymodel, eg the last
|
||||
baymodel from a previous result set. Return
|
||||
the next result set.
|
||||
:param limit: The maximum number of results to return per
|
||||
request, if:
|
||||
|
||||
1) limit > 0, the maximum number of accelerators to return.
|
||||
2) limit == 0, return the entire list of accelerators.
|
||||
3) limit param is NOT specified (None), the number of items
|
||||
returned respect the maximum imposed by the Cyborg API
|
||||
(see Cyborg's api.max_limit option).
|
||||
|
||||
:param sort_key: Optional, field used for sorting.
|
||||
|
||||
:param sort_dir: Optional, direction of sorting, either 'asc' (the
|
||||
default) or 'desc'.
|
||||
|
||||
:param detail: Optional, boolean whether to return detailed information
|
||||
about accelerators.
|
||||
|
||||
:returns: A list of accelerators.
|
||||
|
||||
"""
|
||||
if limit is not None:
|
||||
limit = int(limit)
|
||||
|
||||
filters = utils.common_filters(marker, limit, sort_key, sort_dir)
|
||||
|
||||
path = ''
|
||||
if detail:
|
||||
path += 'detail'
|
||||
if filters:
|
||||
path += '?' + '&'.join(filters)
|
||||
|
||||
if limit is None:
|
||||
return self._list(self._path(path), self.__class__.api_name)
|
||||
else:
|
||||
return self._list_pagination(self._path(path),
|
||||
self.__class__.api_name,
|
||||
limit=limit)
|
||||
|
||||
def get(self, id):
|
||||
try:
|
||||
return self._list(self._path(id))[0]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def create(self, **kwargs):
|
||||
new = {}
|
||||
for (key, value) in kwargs.items():
|
||||
if key in CREATION_ATTRIBUTES:
|
||||
new[key] = value
|
||||
else:
|
||||
raise exceptions.InvalidAttribute(
|
||||
"Key must be in %s" % ",".join(CREATION_ATTRIBUTES))
|
||||
return self._create(self._path(), new)
|
||||
|
||||
def delete(self, id):
|
||||
return self._delete(self._path(id))
|
||||
|
||||
def update(self, id, patch):
|
||||
return self._update(self._path(id), patch)
|
190
cyborgclient/v1/client.py
Normal file
190
cyborgclient/v1/client.py
Normal file
@ -0,0 +1,190 @@
|
||||
# Copyright 2014
|
||||
# The Cloudscaling Group, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from keystoneauth1 import session as ksa_session
|
||||
import os_client_config
|
||||
from oslo_utils import importutils
|
||||
|
||||
from cyborgclient.common import httpclient
|
||||
from cyborgclient.v1 import accelerators
|
||||
|
||||
profiler = importutils.try_import("osprofiler.profiler")
|
||||
|
||||
|
||||
DEFAULT_SERVICE_TYPE = 'accelerator'
|
||||
|
||||
|
||||
def _load_session(cloud=None, insecure=False, timeout=None, **kwargs):
|
||||
cloud_config = os_client_config.OpenStackConfig()
|
||||
cloud_config = cloud_config.get_one_cloud(
|
||||
cloud=cloud,
|
||||
verify=not insecure,
|
||||
**kwargs)
|
||||
verify, cert = cloud_config.get_requests_verify_args()
|
||||
|
||||
auth = cloud_config.get_auth()
|
||||
session = ksa_session.Session(
|
||||
auth=auth, verify=verify, cert=cert,
|
||||
timeout=timeout)
|
||||
|
||||
return session
|
||||
|
||||
|
||||
def _load_service_type(session,
|
||||
service_type=None, service_name=None,
|
||||
interface=None, region_name=None):
|
||||
try:
|
||||
# Trigger an auth error so that we can throw the exception
|
||||
# we always have
|
||||
session.get_endpoint(
|
||||
service_type=service_type,
|
||||
service_name=service_name,
|
||||
interface=interface,
|
||||
region_name=region_name)
|
||||
except Exception as e:
|
||||
raise RuntimeError(str(e))
|
||||
|
||||
return service_type
|
||||
|
||||
|
||||
def _load_session_client(session=None, endpoint_override=None, username=None,
|
||||
project_id=None, project_name=None,
|
||||
auth_url=None, password=None, auth_type=None,
|
||||
insecure=None, user_domain_id=None,
|
||||
user_domain_name=None, project_domain_id=None,
|
||||
project_domain_name=None, auth_token=None,
|
||||
timeout=None, service_type=None, service_name=None,
|
||||
interface=None, region_name=None, api_version=None,
|
||||
**kwargs):
|
||||
if not session:
|
||||
session = _load_session(
|
||||
username=username,
|
||||
project_id=project_id,
|
||||
project_name=project_name,
|
||||
auth_url=auth_url,
|
||||
password=password,
|
||||
auth_type=auth_type,
|
||||
insecure=insecure,
|
||||
user_domain_id=user_domain_id,
|
||||
user_domain_name=user_domain_name,
|
||||
project_domain_id=project_domain_id,
|
||||
project_domain_name=project_domain_name,
|
||||
auth_token=auth_token,
|
||||
timeout=timeout,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
if not endpoint_override:
|
||||
service_type = _load_service_type(
|
||||
session,
|
||||
service_type=service_type,
|
||||
service_name=service_name,
|
||||
interface=interface,
|
||||
region_name=region_name,
|
||||
)
|
||||
|
||||
return httpclient.SessionClient(
|
||||
service_type=service_type,
|
||||
service_name=service_name,
|
||||
interface=interface,
|
||||
region_name=region_name,
|
||||
session=session,
|
||||
endpoint_override=endpoint_override,
|
||||
api_version=api_version,
|
||||
)
|
||||
|
||||
|
||||
class Client(object):
|
||||
def __init__(self, username=None, api_key=None, project_id=None,
|
||||
project_name=None, auth_url=None, cyborg_url=None,
|
||||
endpoint_type=None, endpoint_override=None,
|
||||
service_type=DEFAULT_SERVICE_TYPE,
|
||||
region_name=None, input_auth_token=None,
|
||||
session=None, password=None, auth_type='password',
|
||||
interface=None, service_name=None, insecure=False,
|
||||
user_domain_id=None, user_domain_name=None,
|
||||
project_domain_id=None, project_domain_name=None,
|
||||
auth_token=None, timeout=600, api_version=None,
|
||||
**kwargs):
|
||||
|
||||
# We have to keep the api_key are for backwards compat, but let's
|
||||
# remove it from the rest of our code since it's not a keystone
|
||||
# concept
|
||||
if not password:
|
||||
password = api_key
|
||||
# Backwards compat for people passing in input_auth_token
|
||||
if input_auth_token:
|
||||
auth_token = input_auth_token
|
||||
# Backwards compat for people passing in endpoint_type
|
||||
if endpoint_type:
|
||||
interface = endpoint_type
|
||||
|
||||
# osc sometimes give 'None' value
|
||||
if not interface:
|
||||
interface = 'public'
|
||||
|
||||
if interface.endswith('URL'):
|
||||
interface = interface[:-3]
|
||||
|
||||
# fix (yolanda): os-cloud-config is using endpoint_override
|
||||
# instead of cyborg_url
|
||||
if cyborg_url and not endpoint_override:
|
||||
endpoint_override = cyborg_url
|
||||
|
||||
if endpoint_override and auth_token:
|
||||
self.http_client = httpclient.HTTPClient(
|
||||
endpoint_override,
|
||||
token=auth_token,
|
||||
api_version=api_version,
|
||||
timeout=timeout,
|
||||
insecure=insecure,
|
||||
**kwargs
|
||||
)
|
||||
else:
|
||||
self.http_client = _load_session_client(
|
||||
session=session,
|
||||
endpoint_override=endpoint_override,
|
||||
username=username,
|
||||
project_id=project_id,
|
||||
project_name=project_name,
|
||||
auth_url=auth_url,
|
||||
password=password,
|
||||
auth_type=auth_type,
|
||||
insecure=insecure,
|
||||
user_domain_id=user_domain_id,
|
||||
user_domain_name=user_domain_name,
|
||||
project_domain_id=project_domain_id,
|
||||
project_domain_name=project_domain_name,
|
||||
auth_token=auth_token,
|
||||
timeout=timeout,
|
||||
service_type=service_type,
|
||||
service_name=service_name,
|
||||
interface=interface,
|
||||
region_name=region_name,
|
||||
api_version=api_version,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
self.accelerators = accelerators.AcceleratorManager(self.http_client)
|
||||
|
||||
profile = kwargs.pop("profile", None)
|
||||
if profiler and profile:
|
||||
# Initialize the root of the future trace: the created trace ID
|
||||
# will be used as the very first parent to which all related
|
||||
# traces will be bound to. The given HMAC key must correspond to
|
||||
# the one set in cyborg-api cyborg.conf, otherwise the latter
|
||||
# will fail to check the request signature and will skip
|
||||
# initialization of osprofiler on the server side.
|
||||
profiler.init(profile)
|
20
cyborgclient/v1/shell.py
Normal file
20
cyborgclient/v1/shell.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Copyright 2014
|
||||
# The Cloudscaling Group, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from cyborgclient.v1 import accelerators_shell
|
||||
|
||||
COMMAND_MODULES = [
|
||||
accelerators_shell,
|
||||
]
|
17
cyborgclient/version.py
Normal file
17
cyborgclient/version.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Copyright (c) 2018 Intel, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from pbr import version
|
||||
|
||||
version_info = version.VersionInfo('python-cyborgclient')
|
@ -1,5 +1,19 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
||||
|
||||
pbr>=2.0 # Apache-2.0
|
||||
Babel!=2.4.0,>=2.3.4 # BSD
|
||||
six>=1.10.0 # MIT
|
||||
keystoneauth1>=3.3.0 # Apache-2.0
|
||||
stevedore>=1.20.0 # Apache-2.0
|
||||
requests>=2.14.2 # Apache-2.0
|
||||
oslo.i18n>=3.15.3 # Apache-2.0
|
||||
oslo.log>=3.36.0 # Apache-2.0
|
||||
oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0
|
||||
oslo.utils>=3.33.0 # Apache-2.0
|
||||
os-client-config>=1.28.0 # Apache-2.0
|
||||
osc-lib>=1.8.0 # Apache-2.0
|
||||
PrettyTable<0.8,>=0.7.1 # BSD
|
||||
cryptography!=2.0,>=1.9 # BSD/Apache-2.0
|
||||
decorator>=3.4.0 # BSD
|
||||
|
10
setup.cfg
10
setup.cfg
@ -22,6 +22,16 @@ classifier =
|
||||
packages =
|
||||
cyborgclient
|
||||
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
cyborg = cyborgclient.shell:main
|
||||
|
||||
openstack.cli.extension =
|
||||
accelerator = cyborgclient.osc.plugin
|
||||
|
||||
openstack.accelerator.v1 =
|
||||
accelerator_list = cyborgclient.osc.v1.accelerator:ListAccelerator
|
||||
|
||||
[build_sphinx]
|
||||
all-files = 1
|
||||
warning-is-error = 1
|
||||
|
Loading…
x
Reference in New Issue
Block a user