summaryrefslogtreecommitdiffstats
path: root/client/escalatorclient/common/utils.py
diff options
context:
space:
mode:
Diffstat (limited to 'client/escalatorclient/common/utils.py')
-rw-r--r--client/escalatorclient/common/utils.py462
1 files changed, 462 insertions, 0 deletions
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