summaryrefslogtreecommitdiffstats
path: root/client/escalatorclient/common
diff options
context:
space:
mode:
Diffstat (limited to 'client/escalatorclient/common')
-rw-r--r--client/escalatorclient/common/__init__.py0
-rw-r--r--client/escalatorclient/common/base.py34
-rw-r--r--client/escalatorclient/common/http.py288
-rw-r--r--client/escalatorclient/common/https.py349
-rw-r--r--client/escalatorclient/common/utils.py462
5 files changed, 1133 insertions, 0 deletions
diff --git a/client/escalatorclient/common/__init__.py b/client/escalatorclient/common/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/client/escalatorclient/common/__init__.py
diff --git a/client/escalatorclient/common/base.py b/client/escalatorclient/common/base.py
new file mode 100644
index 0000000..b85dc19
--- /dev/null
+++ b/client/escalatorclient/common/base.py
@@ -0,0 +1,34 @@
+# Copyright 2012 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.
+
+DEPRECATED post v.0.12.0. Use 'escalatorclient.openstack.common.apiclient.base'
+instead of this module."
+"""
+
+import warnings
+
+from escalatorclient.openstack.common.apiclient import base
+
+
+warnings.warn("The 'escalatorclient.common.base' module is deprecated",
+ DeprecationWarning)
+
+
+getid = base.getid
+Manager = base.ManagerWithFind
+Resource = base.Resource
diff --git a/client/escalatorclient/common/http.py b/client/escalatorclient/common/http.py
new file mode 100644
index 0000000..301eedb
--- /dev/null
+++ b/client/escalatorclient/common/http.py
@@ -0,0 +1,288 @@
+# Copyright 2012 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.
+
+import copy
+import logging
+import socket
+from oslo_utils import encodeutils
+from escalatorclient.common import https
+from escalatorclient.common.utils import safe_header
+from escalatorclient import exc
+from oslo_utils import importutils
+from oslo_utils import netutils
+from simplejson import decoder
+import requests
+try:
+ from requests.packages.urllib3.exceptions import ProtocolError
+except ImportError:
+ ProtocolError = requests.exceptions.ConnectionError
+import six
+from six.moves.urllib import parse
+
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+# Python 2.5 compat fix
+if not hasattr(parse, 'parse_qsl'):
+ import cgi
+ parse.parse_qsl = cgi.parse_qsl
+
+
+osprofiler_web = importutils.try_import("osprofiler.web")
+
+LOG = logging.getLogger(__name__)
+USER_AGENT = 'python-escalatorclient'
+CHUNKSIZE = 1024 * 64 # 64kB
+
+
+class HTTPClient(object):
+
+ def __init__(self, endpoint, **kwargs):
+ self.endpoint = endpoint
+ self.identity_headers = kwargs.get('identity_headers')
+ self.auth_token = kwargs.get('token')
+ if self.identity_headers:
+ if self.identity_headers.get('X-Auth-Token'):
+ self.auth_token = self.identity_headers.get('X-Auth-Token')
+ del self.identity_headers['X-Auth-Token']
+
+ self.session = requests.Session()
+ self.session.headers["User-Agent"] = USER_AGENT
+
+ if self.auth_token:
+ self.session.headers["X-Auth-Token"] = self.auth_token
+
+ self.timeout = float(kwargs.get('timeout', 600))
+
+ if self.endpoint.startswith("https"):
+ compression = kwargs.get('ssl_compression', True)
+
+ if not compression:
+ self.session.mount("escalator+https://", https.HTTPSAdapter())
+ self.endpoint = 'escalator+' + self.endpoint
+
+ self.session.verify = (
+ kwargs.get('cacert', requests.certs.where()),
+ kwargs.get('insecure', False))
+
+ else:
+ if kwargs.get('insecure', False) is True:
+ self.session.verify = False
+ else:
+ if kwargs.get('cacert', None) is not '':
+ self.session.verify = kwargs.get('cacert', True)
+
+ self.session.cert = (kwargs.get('cert_file'),
+ kwargs.get('key_file'))
+
+ @staticmethod
+ def parse_endpoint(endpoint):
+ return netutils.urlsplit(endpoint)
+
+ def log_curl_request(self, method, url, headers, data, kwargs):
+ curl = ['curl -g -i -X %s' % method]
+
+ headers = copy.deepcopy(headers)
+ headers.update(self.session.headers)
+
+ for (key, value) in six.iteritems(headers):
+ header = '-H \'%s: %s\'' % safe_header(key, value)
+ curl.append(header)
+
+ if not self.session.verify:
+ curl.append('-k')
+ else:
+ if isinstance(self.session.verify, six.string_types):
+ curl.append(' --cacert %s' % self.session.verify)
+
+ if self.session.cert:
+ curl.append(' --cert %s --key %s' % self.session.cert)
+
+ if data and isinstance(data, six.string_types):
+ curl.append('-d \'%s\'' % data)
+
+ curl.append(url)
+
+ msg = ' '.join([encodeutils.safe_decode(item, errors='ignore')
+ for item in curl])
+ LOG.debug(msg)
+
+ @staticmethod
+ def log_http_response(resp, body=None):
+ status = (resp.raw.version / 10.0, resp.status_code, resp.reason)
+ dump = ['\nHTTP/%.1f %s %s' % status]
+ headers = resp.headers.items()
+ dump.extend(['%s: %s' % safe_header(k, v) for k, v in headers])
+ dump.append('')
+ if body:
+ body = encodeutils.safe_decode(body)
+ dump.extend([body, ''])
+ LOG.debug('\n'.join([encodeutils.safe_decode(x, errors='ignore')
+ for x in dump]))
+
+ @staticmethod
+ def encode_headers(headers):
+ """Encodes headers.
+
+ Note: This should be used right before
+ sending anything out.
+
+ :param headers: Headers to encode
+ :returns: Dictionary with encoded headers'
+ names and values
+ """
+ return dict((encodeutils.safe_encode(h), encodeutils.safe_encode(v))
+ for h, v in six.iteritems(headers) if v is not None)
+
+ def _request(self, method, url, **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
+ headers = kwargs.pop("headers", {})
+ headers = headers and copy.deepcopy(headers) or {}
+
+ if self.identity_headers:
+ for k, v in six.iteritems(self.identity_headers):
+ headers.setdefault(k, v)
+
+ # Default Content-Type is octet-stream
+ content_type = headers.get('Content-Type', 'application/octet-stream')
+
+ def chunk_body(body):
+ chunk = body
+ while chunk:
+ chunk = body.read(CHUNKSIZE)
+ if chunk == '':
+ break
+ yield chunk
+
+ data = kwargs.pop("data", None)
+ if data is not None and not isinstance(data, six.string_types):
+ try:
+ data = json.dumps(data)
+ content_type = 'application/json'
+ except TypeError:
+ # Here we assume it's
+ # a file-like object
+ # and we'll chunk it
+ data = chunk_body(data)
+
+ headers['Content-Type'] = content_type
+ stream = True if content_type == 'application/octet-stream' else False
+
+ if osprofiler_web:
+ headers.update(osprofiler_web.get_trace_id_headers())
+
+ # Note(flaper87): Before letting headers / url fly,
+ # they should be encoded otherwise httplib will
+ # complain.
+ headers = self.encode_headers(headers)
+
+ try:
+ if self.endpoint.endswith("/") or url.startswith("/"):
+ conn_url = "%s%s" % (self.endpoint, url)
+ else:
+ conn_url = "%s/%s" % (self.endpoint, url)
+ self.log_curl_request(method, conn_url, headers, data, kwargs)
+ resp = self.session.request(method,
+ conn_url,
+ data=data,
+ stream=stream,
+ headers=headers,
+ **kwargs)
+ except requests.exceptions.Timeout as e:
+ message = ("Error communicating with %(endpoint)s %(e)s" %
+ dict(url=conn_url, e=e))
+ raise exc.InvalidEndpoint(message=message)
+ except (requests.exceptions.ConnectionError, ProtocolError) as e:
+ message = ("Error finding address for %(url)s: %(e)s" %
+ dict(url=conn_url, e=e))
+ raise exc.CommunicationError(message=message)
+ except socket.gaierror as e:
+ message = "Error finding address for %s: %s" % (
+ self.endpoint_hostname, e)
+ raise exc.InvalidEndpoint(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.CommunicationError(message=message)
+
+ if not resp.ok:
+ LOG.debug("Request returned failure status %s." % resp.status_code)
+ raise exc.from_response(resp, resp.text)
+ elif resp.status_code == requests.codes.MULTIPLE_CHOICES:
+ raise exc.from_response(resp)
+
+ content_type = resp.headers.get('Content-Type')
+
+ # Read body into string if it isn't obviously image data
+ if content_type == 'application/octet-stream':
+ # Do not read all response in memory when
+ # downloading an image.
+ body_iter = _close_after_stream(resp, CHUNKSIZE)
+ self.log_http_response(resp)
+ else:
+ content = resp.text
+ self.log_http_response(resp, content)
+ if content_type and content_type.startswith('application/json'):
+ # Let's use requests json method,
+ # it should take care of response
+ # encoding
+ try:
+ body_iter = resp.json()
+ except decoder.JSONDecodeError:
+ status_body = {'status_code': resp.status_code}
+ return resp, status_body
+ else:
+ body_iter = six.StringIO(content)
+ try:
+ body_iter = json.loads(''.join([c for c in body_iter]))
+ except ValueError:
+ body_iter = None
+ return resp, body_iter
+
+ def head(self, url, **kwargs):
+ return self._request('HEAD', url, **kwargs)
+
+ def get(self, url, **kwargs):
+ return self._request('GET', url, **kwargs)
+
+ def post(self, url, **kwargs):
+ return self._request('POST', url, **kwargs)
+
+ def put(self, url, **kwargs):
+ return self._request('PUT', url, **kwargs)
+
+ def patch(self, url, **kwargs):
+ return self._request('PATCH', url, **kwargs)
+
+ def delete(self, url, **kwargs):
+ return self._request('DELETE', url, **kwargs)
+
+
+def _close_after_stream(response, chunk_size):
+ """Iterate over the content and ensure the response is closed after."""
+ # Yield each chunk in the response body
+ for chunk in response.iter_content(chunk_size=chunk_size):
+ yield chunk
+ # Once we're done streaming the body, ensure everything is closed.
+ # This will return the connection to the HTTPConnectionPool in urllib3
+ # and ideally reduce the number of HTTPConnectionPool full warnings.
+ response.close()
diff --git a/client/escalatorclient/common/https.py b/client/escalatorclient/common/https.py
new file mode 100644
index 0000000..55769a0
--- /dev/null
+++ b/client/escalatorclient/common/https.py
@@ -0,0 +1,349 @@
+# Copyright 2014 Red Hat, 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 socket
+import ssl
+import struct
+
+import OpenSSL
+from requests import adapters
+from requests import compat
+try:
+ from requests.packages.urllib3 import connectionpool
+except ImportError:
+ from urllib3 import connectionpool
+
+from oslo_utils import encodeutils
+import six
+# NOTE(jokke): simplified transition to py3, behaves like py2 xrange
+from six.moves import range
+
+from escalatorclient.common import utils
+
+try:
+ from eventlet import patcher
+ # Handle case where we are running in a monkey patched environment
+ if patcher.is_monkey_patched('socket'):
+ from eventlet.green.httplib import HTTPSConnection
+ from eventlet.green.OpenSSL.SSL import GreenConnection as Connection
+ from eventlet.greenio import GreenSocket
+ # TODO(mclaren): A getsockopt workaround: see 'getsockopt' doc string
+ GreenSocket.getsockopt = utils.getsockopt
+ else:
+ raise ImportError
+except ImportError:
+ try:
+ from httplib import HTTPSConnection
+ except ImportError:
+ from http.client import HTTPSConnection
+ from OpenSSL.SSL import Connection as Connection
+
+
+from escalatorclient import exc
+
+
+def verify_callback(host=None):
+ """
+ We use a partial around the 'real' verify_callback function
+ so that we can stash the host value without holding a
+ reference on the VerifiedHTTPSConnection.
+ """
+ def wrapper(connection, x509, errnum,
+ depth, preverify_ok, host=host):
+ return do_verify_callback(connection, x509, errnum,
+ depth, preverify_ok, host=host)
+ return wrapper
+
+
+def do_verify_callback(connection, x509, errnum,
+ depth, preverify_ok, host=None):
+ """
+ Verify the server's SSL certificate.
+
+ This is a standalone function rather than a method to avoid
+ issues around closing sockets if a reference is held on
+ a VerifiedHTTPSConnection by the callback function.
+ """
+ if x509.has_expired():
+ msg = "SSL Certificate expired on '%s'" % x509.get_notAfter()
+ raise exc.SSLCertificateError(msg)
+
+ if depth == 0 and preverify_ok:
+ # We verify that the host matches against the last
+ # certificate in the chain
+ return host_matches_cert(host, x509)
+ else:
+ # Pass through OpenSSL's default result
+ return preverify_ok
+
+
+def host_matches_cert(host, x509):
+ """
+ Verify that the x509 certificate we have received
+ from 'host' correctly identifies the server we are
+ connecting to, ie that the certificate's Common Name
+ or a Subject Alternative Name matches 'host'.
+ """
+ def check_match(name):
+ # Directly match the name
+ if name == host:
+ return True
+
+ # Support single wildcard matching
+ if name.startswith('*.') and host.find('.') > 0:
+ if name[2:] == host.split('.', 1)[1]:
+ return True
+
+ common_name = x509.get_subject().commonName
+
+ # First see if we can match the CN
+ if check_match(common_name):
+ return True
+ # Also try Subject Alternative Names for a match
+ san_list = None
+ for i in range(x509.get_extension_count()):
+ ext = x509.get_extension(i)
+ if ext.get_short_name() == b'subjectAltName':
+ san_list = str(ext)
+ for san in ''.join(san_list.split()).split(','):
+ if san.startswith('DNS:'):
+ if check_match(san.split(':', 1)[1]):
+ return True
+
+ # Server certificate does not match host
+ msg = ('Host "%s" does not match x509 certificate contents: '
+ 'CommonName "%s"' % (host, common_name))
+ if san_list is not None:
+ msg = msg + ', subjectAltName "%s"' % san_list
+ raise exc.SSLCertificateError(msg)
+
+
+def to_bytes(s):
+ if isinstance(s, six.string_types):
+ return six.b(s)
+ else:
+ return s
+
+
+class HTTPSAdapter(adapters.HTTPAdapter):
+ """
+ This adapter will be used just when
+ ssl compression should be disabled.
+
+ The init method overwrites the default
+ https pool by setting escalatorclient's
+ one.
+ """
+
+ def request_url(self, request, proxies):
+ # NOTE(flaper87): Make sure the url is encoded, otherwise
+ # python's standard httplib will fail with a TypeError.
+ url = super(HTTPSAdapter, self).request_url(request, proxies)
+ return encodeutils.safe_encode(url)
+
+ def _create_escalator_httpsconnectionpool(self, url):
+ kw = self.poolmanager.connection_kw
+ # Parse the url to get the scheme, host, and port
+ parsed = compat.urlparse(url)
+ # If there is no port specified, we should use the standard HTTPS port
+ port = parsed.port or 443
+ pool = HTTPSConnectionPool(parsed.host, port, **kw)
+
+ with self.poolmanager.pools.lock:
+ self.poolmanager.pools[(parsed.scheme, parsed.host, port)] = pool
+
+ return pool
+
+ def get_connection(self, url, proxies=None):
+ try:
+ return super(HTTPSAdapter, self).get_connection(url, proxies)
+ except KeyError:
+ # NOTE(sigamvirus24): This works around modifying a module global
+ # which fixes bug #1396550
+ # The scheme is most likely escalator+https but check anyway
+ if not url.startswith('escalator+https://'):
+ raise
+
+ return self._create_escalator_httpsconnectionpool(url)
+
+ def cert_verify(self, conn, url, verify, cert):
+ super(HTTPSAdapter, self).cert_verify(conn, url, verify, cert)
+ conn.ca_certs = verify[0]
+ conn.insecure = verify[1]
+
+
+class HTTPSConnectionPool(connectionpool.HTTPSConnectionPool):
+ """
+ HTTPSConnectionPool will be instantiated when a new
+ connection is requested to the HTTPSAdapter.This
+ implementation overwrites the _new_conn method and
+ returns an instances of escalatorclient's VerifiedHTTPSConnection
+ which handles no compression.
+
+ ssl_compression is hard-coded to False because this will
+ be used just when the user sets --no-ssl-compression.
+ """
+
+ scheme = 'escalator+https'
+
+ def _new_conn(self):
+ self.num_connections += 1
+ return VerifiedHTTPSConnection(host=self.host,
+ port=self.port,
+ key_file=self.key_file,
+ cert_file=self.cert_file,
+ cacert=self.ca_certs,
+ insecure=self.insecure,
+ ssl_compression=False)
+
+
+class OpenSSLConnectionDelegator(object):
+ """
+ An OpenSSL.SSL.Connection delegator.
+
+ Supplies an additional 'makefile' method which httplib requires
+ and is not present in OpenSSL.SSL.Connection.
+
+ Note: Since it is not possible to inherit from OpenSSL.SSL.Connection
+ a delegator must be used.
+ """
+ def __init__(self, *args, **kwargs):
+ self.connection = Connection(*args, **kwargs)
+
+ def __getattr__(self, name):
+ return getattr(self.connection, name)
+
+ def makefile(self, *args, **kwargs):
+ return socket._fileobject(self.connection, *args, **kwargs)
+
+
+class VerifiedHTTPSConnection(HTTPSConnection):
+ """
+ Extended HTTPSConnection which uses the OpenSSL library
+ for enhanced SSL support.
+ Note: Much of this functionality can eventually be replaced
+ with native Python 3.3 code.
+ """
+ # Restrict the set of client supported cipher suites
+ CIPHERS = 'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:'\
+ 'eCDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:'\
+ 'RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS'
+
+ def __init__(self, host, port=None, key_file=None, cert_file=None,
+ cacert=None, timeout=None, insecure=False,
+ ssl_compression=True):
+ # List of exceptions reported by Python3 instead of
+ # SSLConfigurationError
+ if six.PY3:
+ excp_lst = (TypeError, IOError, ssl.SSLError)
+ # https.py:250:36: F821 undefined name 'FileNotFoundError'
+ else:
+ # NOTE(jamespage)
+ # Accomodate changes in behaviour for pep-0467, introduced
+ # in python 2.7.9.
+ # https://github.com/python/peps/blob/master/pep-0476.txt
+ excp_lst = (TypeError, IOError, ssl.SSLError)
+ try:
+ HTTPSConnection.__init__(self, host, port,
+ key_file=key_file,
+ cert_file=cert_file)
+ self.key_file = key_file
+ self.cert_file = cert_file
+ self.timeout = timeout
+ self.insecure = insecure
+ # NOTE(flaper87): `is_verified` is needed for
+ # requests' urllib3. If insecure is True then
+ # the request is not `verified`, hence `not insecure`
+ self.is_verified = not insecure
+ self.ssl_compression = ssl_compression
+ self.cacert = None if cacert is None else str(cacert)
+ self.set_context()
+ # ssl exceptions are reported in various form in Python 3
+ # so to be compatible, we report the same kind as under
+ # Python2
+ except excp_lst as e:
+ raise exc.SSLConfigurationError(str(e))
+
+ def set_context(self):
+ """
+ Set up the OpenSSL context.
+ """
+ self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
+ self.context.set_cipher_list(self.CIPHERS)
+
+ if self.ssl_compression is False:
+ self.context.set_options(0x20000) # SSL_OP_NO_COMPRESSION
+
+ if self.insecure is not True:
+ self.context.set_verify(OpenSSL.SSL.VERIFY_PEER,
+ verify_callback(host=self.host))
+ else:
+ self.context.set_verify(OpenSSL.SSL.VERIFY_NONE,
+ lambda *args: True)
+
+ if self.cert_file:
+ try:
+ self.context.use_certificate_file(self.cert_file)
+ except Exception as e:
+ msg = 'Unable to load cert from "%s" %s' % (self.cert_file, e)
+ raise exc.SSLConfigurationError(msg)
+ if self.key_file is None:
+ # We support having key and cert in same file
+ try:
+ self.context.use_privatekey_file(self.cert_file)
+ except Exception as e:
+ msg = ('No key file specified and unable to load key '
+ 'from "%s" %s' % (self.cert_file, e))
+ raise exc.SSLConfigurationError(msg)
+
+ if self.key_file:
+ try:
+ self.context.use_privatekey_file(self.key_file)
+ except Exception as e:
+ msg = 'Unable to load key from "%s" %s' % (self.key_file, e)
+ raise exc.SSLConfigurationError(msg)
+
+ if self.cacert:
+ try:
+ self.context.load_verify_locations(to_bytes(self.cacert))
+ except Exception as e:
+ msg = 'Unable to load CA from "%s" %s' % (self.cacert, e)
+ raise exc.SSLConfigurationError(msg)
+ else:
+ self.context.set_default_verify_paths()
+
+ def connect(self):
+ """
+ Connect to an SSL port using the OpenSSL library and apply
+ per-connection parameters.
+ """
+ result = socket.getaddrinfo(self.host, self.port, 0,
+ socket.SOCK_STREAM)
+ if result:
+ socket_family = result[0][0]
+ if socket_family == socket.AF_INET6:
+ sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
+ else:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ else:
+ # If due to some reason the address lookup fails - we still connect
+ # to IPv4 socket. This retains the older behavior.
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ if self.timeout is not None:
+ # '0' microseconds
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO,
+ struct.pack('LL', self.timeout, 0))
+ self.sock = OpenSSLConnectionDelegator(self.context, sock)
+ self.sock.connect((self.host, self.port))
diff --git a/client/escalatorclient/common/utils.py b/client/escalatorclient/common/utils.py
new file mode 100644
index 0000000..0156d31
--- /dev/null
+++ b/client/escalatorclient/common/utils.py
@@ -0,0 +1,462 @@
+# Copyright 2012 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.
+
+from __future__ import print_function
+
+import errno
+import hashlib
+import json
+import os
+import re
+import sys
+import threading
+import uuid
+from oslo_utils import encodeutils
+from oslo_utils import strutils
+import prettytable
+import six
+
+from escalatorclient import exc
+from oslo_utils import importutils
+
+if os.name == 'nt':
+ import msvcrt
+else:
+ msvcrt = None
+
+
+_memoized_property_lock = threading.Lock()
+
+SENSITIVE_HEADERS = ('X-Auth-Token', )
+
+
+# Decorator for cli-args
+def arg(*args, **kwargs):
+ def _decorator(func):
+ # Because of the sematics of decorator composition if we just append
+ # to the options list positional options will appear to be backwards.
+ func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
+ return func
+ return _decorator
+
+
+def schema_args(schema_getter, omit=None):
+ omit = omit or []
+ typemap = {
+ 'string': str,
+ 'integer': int,
+ 'boolean': strutils.bool_from_string,
+ 'array': list
+ }
+
+ def _decorator(func):
+ schema = schema_getter()
+ if schema is None:
+ param = '<unavailable>'
+ kwargs = {
+ 'help': ("Please run with connection parameters set to "
+ "retrieve the schema for generating help for this "
+ "command")
+ }
+ func.__dict__.setdefault('arguments', []).insert(0, ((param, ),
+ kwargs))
+ else:
+ properties = schema.get('properties', {})
+ for name, property in six.iteritems(properties):
+ if name in omit:
+ continue
+ param = '--' + name.replace('_', '-')
+ kwargs = {}
+
+ type_str = property.get('type', 'string')
+
+ if isinstance(type_str, list):
+ # NOTE(flaper87): This means the server has
+ # returned something like `['null', 'string']`,
+ # therfore we use the first non-`null` type as
+ # the valid type.
+ for t in type_str:
+ if t != 'null':
+ type_str = t
+ break
+
+ if type_str == 'array':
+ items = property.get('items')
+ kwargs['type'] = typemap.get(items.get('type'))
+ kwargs['nargs'] = '+'
+ else:
+ kwargs['type'] = typemap.get(type_str)
+
+ if type_str == 'boolean':
+ kwargs['metavar'] = '[True|False]'
+ else:
+ kwargs['metavar'] = '<%s>' % name.upper()
+
+ description = property.get('description', "")
+ if 'enum' in property:
+ if len(description):
+ description += " "
+
+ # NOTE(flaper87): Make sure all values are `str/unicode`
+ # for the `join` to succeed. Enum types can also be `None`
+ # therfore, join's call would fail without the following
+ # list comprehension
+ vals = [six.text_type(val) for val in property.get('enum')]
+ description += ('Valid values: ' + ', '.join(vals))
+ kwargs['help'] = description
+
+ func.__dict__.setdefault('arguments',
+ []).insert(0, ((param, ), kwargs))
+ return func
+
+ return _decorator
+
+
+def pretty_choice_list(l):
+ return ', '.join("'%s'" % i for i in l)
+
+
+def print_list(objs, fields, formatters=None, field_settings=None,
+ conver_field=True):
+ formatters = formatters or {}
+ field_settings = field_settings or {}
+ pt = prettytable.PrettyTable([f for f in fields], caching=False)
+ pt.align = 'l'
+
+ for o in objs:
+ row = []
+ for field in fields:
+ if field in field_settings:
+ for setting, value in six.iteritems(field_settings[field]):
+ setting_dict = getattr(pt, setting)
+ setting_dict[field] = value
+
+ if field in formatters:
+ row.append(formatters[field](o))
+ else:
+ if conver_field:
+ field_name = field.lower().replace(' ', '_')
+ else:
+ field_name = field.replace(' ', '_')
+ data = getattr(o, field_name, None)
+ row.append(data)
+ pt.add_row(row)
+
+ print(encodeutils.safe_decode(pt.get_string()))
+
+
+def print_dict(d, max_column_width=80):
+ pt = prettytable.PrettyTable(['Property', 'Value'], caching=False)
+ pt.align = 'l'
+ pt.max_width = max_column_width
+ for k, v in six.iteritems(d):
+ if isinstance(v, (dict, list)):
+ v = json.dumps(v)
+ pt.add_row([k, v])
+ print(encodeutils.safe_decode(pt.get_string(sortby='Property')))
+
+
+def find_resource(manager, id):
+ """Helper for the _find_* methods."""
+ # first try to get entity as integer id
+ try:
+ if isinstance(id, int) or id.isdigit():
+ return manager.get(int(id))
+ except exc.NotFound:
+ pass
+
+ # now try to get entity as uuid
+ try:
+ # This must be unicode for Python 3 compatibility.
+ # If you pass a bytestring to uuid.UUID, you will get a TypeError
+ uuid.UUID(encodeutils.safe_decode(id))
+ return manager.get(id)
+ except (ValueError, exc.NotFound):
+ msg = ("id %s is error " % id)
+ raise exc.CommandError(msg)
+
+ # finally try to find entity by name
+ matches = list(manager.list(filters={'name': id}))
+ num_matches = len(matches)
+ if num_matches == 0:
+ msg = "No %s with a name or ID of '%s' exists." % \
+ (manager.resource_class.__name__.lower(), id)
+ raise exc.CommandError(msg)
+ elif num_matches > 1:
+ msg = ("Multiple %s matches found for '%s', use an ID to be more"
+ " specific." % (manager.resource_class.__name__.lower(),
+ id))
+ raise exc.CommandError(msg)
+ else:
+ return matches[0]
+
+
+def skip_authentication(f):
+ """Function decorator used to indicate a caller may be unauthenticated."""
+ f.require_authentication = False
+ return f
+
+
+def is_authentication_required(f):
+ """Checks to see if the function requires authentication.
+
+ Use the skip_authentication decorator to indicate a caller may
+ skip the authentication step.
+ """
+ return getattr(f, 'require_authentication', True)
+
+
+def env(*vars, **kwargs):
+ """Search for the first defined of possibly many env vars
+
+ Returns the first environment variable defined in vars, or
+ returns the default defined in kwargs.
+ """
+ for v in vars:
+ value = os.environ.get(v, None)
+ if value:
+ return value
+ return kwargs.get('default', '')
+
+
+def import_versioned_module(version, submodule=None):
+ module = 'escalatorclient.v%s' % version
+ if submodule:
+ module = '.'.join((module, submodule))
+ return importutils.import_module(module)
+
+
+def exit(msg='', exit_code=1):
+ if msg:
+ print(encodeutils.safe_decode(msg), file=sys.stderr)
+ sys.exit(exit_code)
+
+
+def save_image(data, path):
+ """
+ Save an image to the specified path.
+
+ :param data: binary data of the image
+ :param path: path to save the image to
+ """
+ if path is None:
+ image = sys.stdout
+ else:
+ image = open(path, 'wb')
+ try:
+ for chunk in data:
+ image.write(chunk)
+ finally:
+ if path is not None:
+ image.close()
+
+
+def make_size_human_readable(size):
+ suffix = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB']
+ base = 1024.0
+
+ index = 0
+ while size >= base:
+ index = index + 1
+ size = size / base
+
+ padded = '%.1f' % size
+ stripped = padded.rstrip('0').rstrip('.')
+
+ return '%s%s' % (stripped, suffix[index])
+
+
+def getsockopt(self, *args, **kwargs):
+ """
+ A function which allows us to monkey patch eventlet's
+ GreenSocket, adding a required 'getsockopt' method.
+ TODO: (mclaren) we can remove this once the eventlet fix
+ (https://bitbucket.org/eventlet/eventlet/commits/609f230)
+ lands in mainstream packages.
+ """
+ return self.fd.getsockopt(*args, **kwargs)
+
+
+def exception_to_str(exc):
+ try:
+ error = six.text_type(exc)
+ except UnicodeError:
+ try:
+ error = str(exc)
+ except UnicodeError:
+ error = ("Caught '%(exception)s' exception." %
+ {"exception": exc.__class__.__name__})
+ return encodeutils.safe_decode(error, errors='ignore')
+
+
+def get_file_size(file_obj):
+ """
+ Analyze file-like object and attempt to determine its size.
+
+ :param file_obj: file-like object.
+ :retval The file's size or None if it cannot be determined.
+ """
+ if (hasattr(file_obj, 'seek') and hasattr(file_obj, 'tell') and
+ (six.PY2 or six.PY3 and file_obj.seekable())):
+ try:
+ curr = file_obj.tell()
+ file_obj.seek(0, os.SEEK_END)
+ size = file_obj.tell()
+ file_obj.seek(curr)
+ return size
+ except IOError as e:
+ if e.errno == errno.ESPIPE:
+ # Illegal seek. This means the file object
+ # is a pipe (e.g. the user is trying
+ # to pipe image data to the client,
+ # echo testdata | bin/escalator add blah...), or
+ # that file object is empty, or that a file-like
+ # object which doesn't support 'seek/tell' has
+ # been supplied.
+ return
+ else:
+ raise
+
+
+def get_data_file(args):
+ if args.file:
+ return open(args.file, 'rb')
+ else:
+ # distinguish cases where:
+ # (1) stdin is not valid (as in cron jobs):
+ # escalator ... <&-
+ # (2) image data is provided through standard input:
+ # escalator ... < /tmp/file or cat /tmp/file | escalator ...
+ # (3) no image data provided:
+ # escalator ...
+ try:
+ os.fstat(0)
+ except OSError:
+ # (1) stdin is not valid (closed...)
+ return None
+ if not sys.stdin.isatty():
+ # (2) image data is provided through standard input
+ if msvcrt:
+ msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
+ return sys.stdin
+ else:
+ # (3) no image data provided
+ return None
+
+
+def strip_version(endpoint):
+ """Strip version from the last component of endpoint if present."""
+ # NOTE(flaper87): This shouldn't be necessary if
+ # we make endpoint the first argument. However, we
+ # can't do that just yet because we need to keep
+ # backwards compatibility.
+ if not isinstance(endpoint, six.string_types):
+ raise ValueError("Expected endpoint")
+
+ version = None
+ # Get rid of trailing '/' if present
+ endpoint = endpoint.rstrip('/')
+ url_bits = endpoint.split('/')
+ # regex to match 'v1' or 'v2.0' etc
+ if re.match('v\d+\.?\d*', url_bits[-1]):
+ version = float(url_bits[-1].lstrip('v'))
+ endpoint = '/'.join(url_bits[:-1])
+ return endpoint, version
+
+
+def print_image(image_obj, max_col_width=None):
+ ignore = ['self', 'access', 'file', 'schema']
+ image = dict([item for item in six.iteritems(image_obj)
+ if item[0] not in ignore])
+ if str(max_col_width).isdigit():
+ print_dict(image, max_column_width=max_col_width)
+ else:
+ print_dict(image)
+
+
+def integrity_iter(iter, checksum):
+ """
+ Check image data integrity.
+
+ :raises: IOError
+ """
+ md5sum = hashlib.md5()
+ for chunk in iter:
+ yield chunk
+ if isinstance(chunk, six.string_types):
+ chunk = six.b(chunk)
+ md5sum.update(chunk)
+ md5sum = md5sum.hexdigest()
+ if md5sum != checksum:
+ raise IOError(errno.EPIPE,
+ 'Corrupt image download. Checksum was %s expected %s' %
+ (md5sum, checksum))
+
+
+def memoized_property(fn):
+ attr_name = '_lazy_once_' + fn.__name__
+
+ @property
+ def _memoized_property(self):
+ if hasattr(self, attr_name):
+ return getattr(self, attr_name)
+ else:
+ with _memoized_property_lock:
+ if not hasattr(self, attr_name):
+ setattr(self, attr_name, fn(self))
+ return getattr(self, attr_name)
+ return _memoized_property
+
+
+def safe_header(name, value):
+ if name in SENSITIVE_HEADERS:
+ v = value.encode('utf-8')
+ h = hashlib.sha1(v)
+ d = h.hexdigest()
+ return name, "{SHA1}%s" % d
+ else:
+ return name, value
+
+
+def to_str(value):
+ if value is None:
+ return value
+ if not isinstance(value, six.string_types):
+ return str(value)
+ return value
+
+
+def get_host_min_mac(host_interfaces):
+ mac_list = [interface['mac'] for interface in
+ host_interfaces if interface.get('mac')]
+ if mac_list:
+ return min(mac_list)
+ else:
+ return None
+
+
+class IterableWithLength(object):
+ def __init__(self, iterable, length):
+ self.iterable = iterable
+ self.length = length
+
+ def __iter__(self):
+ return self.iterable
+
+ def next(self):
+ return next(self.iterable)
+
+ def __len__(self):
+ return self.length