From 6c269efda87185a1bbecf22c62dead1cd78132af Mon Sep 17 00:00:00 2001
From: Dean Troyer <dtroyer@gmail.com>
Date: Wed, 8 Jun 2016 14:33:39 -0500
Subject: [PATCH] Use osc-lib and set up deprecation warnings

The initial use of osc-lib is behind the compatibility/deprecation
modules that we will leave in place for a time for plugins to catch
up.

* openstackclient.common.exceptions
* openstackclient.common.utils

Module-level warnings are emitted directly on stderr since logging
has not been configured yet.

Change-Id: I79e57ce9523a20366bccaf9b949ab5906792ea0d
---
 openstackclient/common/exceptions.py | 112 +------
 openstackclient/common/utils.py      | 428 +--------------------------
 requirements.txt                     |   1 +
 3 files changed, 19 insertions(+), 522 deletions(-)

diff --git a/openstackclient/common/exceptions.py b/openstackclient/common/exceptions.py
index bdc33ddb54..7124074c99 100644
--- a/openstackclient/common/exceptions.py
+++ b/openstackclient/common/exceptions.py
@@ -1,5 +1,3 @@
-#   Copyright 2012-2013 OpenStack, LLC.
-#
 #   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
@@ -13,105 +11,15 @@
 #   under the License.
 #
 
-"""Exception definitions."""
+# NOTE(dtroyer): This file is deprecated in Jun 2016, remove after 4.x release
+#                or Jun 2017.
+
+import sys
+
+from osc_lib.exceptions import *  # noqa
 
 
-class CommandError(Exception):
-    pass
-
-
-class AuthorizationFailure(Exception):
-    pass
-
-
-class PluginAttributeError(Exception):
-    """A plugin threw an AttributeError while being lazily loaded."""
-    # This *must not* inherit from AttributeError;
-    # that would defeat the whole purpose.
-    pass
-
-
-class NoTokenLookupException(Exception):
-    """This does not support looking up endpoints from an existing token."""
-    pass
-
-
-class EndpointNotFound(Exception):
-    """Could not find Service or Region in Service Catalog."""
-    pass
-
-
-class UnsupportedVersion(Exception):
-    """The user is trying to use an unsupported version of the API"""
-    pass
-
-
-class ClientException(Exception):
-    """The base exception class for all exceptions this library raises."""
-
-    def __init__(self, code, message=None, details=None):
-        self.code = code
-        self.message = message or self.__class__.message
-        self.details = details
-
-    def __str__(self):
-        return "%s (HTTP %s)" % (self.message, self.code)
-
-
-class BadRequest(ClientException):
-    """HTTP 400 - Bad request: you sent some malformed data."""
-    http_status = 400
-    message = "Bad request"
-
-
-class Unauthorized(ClientException):
-    """HTTP 401 - Unauthorized: bad credentials."""
-    http_status = 401
-    message = "Unauthorized"
-
-
-class Forbidden(ClientException):
-    """HTTP 403 - Forbidden: not authorized to access to this resource."""
-    http_status = 403
-    message = "Forbidden"
-
-
-class NotFound(ClientException):
-    """HTTP 404 - Not found"""
-    http_status = 404
-    message = "Not found"
-
-
-class Conflict(ClientException):
-    """HTTP 409 - Conflict"""
-    http_status = 409
-    message = "Conflict"
-
-
-class OverLimit(ClientException):
-    """HTTP 413 - Over limit: reached the API limits for this time period."""
-    http_status = 413
-    message = "Over limit"
-
-
-# NotImplemented is a python keyword.
-class HTTPNotImplemented(ClientException):
-    """HTTP 501 - Not Implemented: server does not support this operation."""
-    http_status = 501
-    message = "Not Implemented"
-
-
-# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__()
-# so we can do this:
-#     _code_map = dict((c.http_status, c)
-#                      for c in ClientException.__subclasses__())
-#
-# Instead, we have to hardcode it:
-_code_map = dict((c.http_status, c) for c in [
-    BadRequest,
-    Unauthorized,
-    Forbidden,
-    NotFound,
-    OverLimit,
-    HTTPNotImplemented
-])
+sys.stderr.write(
+    "WARNING: %s is deprecated and will be removed after Jun 2017. "
+    "Please use osc_lib.exceptions\n" % __name__
+)
diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py
index 5e058547b4..73cd3dc9d3 100644
--- a/openstackclient/common/utils.py
+++ b/openstackclient/common/utils.py
@@ -1,5 +1,3 @@
-#   Copyright 2012-2013 OpenStack Foundation
-#
 #   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
@@ -13,425 +11,15 @@
 #   under the License.
 #
 
-"""Common client utilities"""
+# NOTE(dtroyer): This file is deprecated in Jun 2016, remove after 4.x release
+#                or Jun 2017.
 
-import getpass
-import logging
-import os
-import six
-import time
+import sys
 
-from oslo_utils import importutils
+from osc_lib.utils import *  # noqa
 
-from openstackclient.common import exceptions
-from openstackclient.i18n import _
 
-
-def find_resource(manager, name_or_id, **kwargs):
-    """Helper for the _find_* methods.
-
-    :param manager: A client manager class
-    :param name_or_id: The resource we are trying to find
-    :param kwargs: To be used in calling .find()
-    :rtype: The found resource
-
-    This method will attempt to find a resource in a variety of ways.
-    Primarily .get() methods will be called with `name_or_id` as an integer
-    value, and tried again as a string value.
-
-    If both fail, then a .find() is attempted, which is essentially calling
-    a .list() function with a 'name' query parameter that is set to
-    `name_or_id`.
-
-    Lastly, if any kwargs are passed in, they will be treated as additional
-    query parameters. This is particularly handy in the case of finding
-    resources in a domain.
-
-    """
-
-    # Try to get entity as integer id
-    try:
-        if isinstance(name_or_id, int) or name_or_id.isdigit():
-            return manager.get(int(name_or_id), **kwargs)
-    # FIXME(dtroyer): The exception to catch here is dependent on which
-    #                 client library the manager passed in belongs to.
-    #                 Eventually this should be pulled from a common set
-    #                 of client exceptions.
-    except Exception as ex:
-        if type(ex).__name__ == 'NotFound':
-            pass
-        else:
-            raise
-
-    # Try directly using the passed value
-    try:
-        return manager.get(name_or_id, **kwargs)
-    except Exception:
-        pass
-
-    if len(kwargs) == 0:
-        kwargs = {}
-
-    try:
-        # Prepare the kwargs for calling find
-        if 'NAME_ATTR' in manager.resource_class.__dict__:
-            # novaclient does this for oddball resources
-            kwargs[manager.resource_class.NAME_ATTR] = name_or_id
-        else:
-            kwargs['name'] = name_or_id
-    except Exception:
-        pass
-
-    # finally try to find entity by name
-    try:
-        return manager.find(**kwargs)
-    # FIXME(dtroyer): The exception to catch here is dependent on which
-    #                 client library the manager passed in belongs to.
-    #                 Eventually this should be pulled from a common set
-    #                 of client exceptions.
-    except Exception as ex:
-        if type(ex).__name__ == 'NotFound':
-            msg = _("No %(resource)s with a name or ID "
-                    "of '%(name_or_id)s' exists.")
-            raise exceptions.CommandError(
-                msg % {'resource': manager.resource_class.__name__.lower(),
-                       'name_or_id': name_or_id}
-            )
-        if type(ex).__name__ == 'NoUniqueMatch':
-            msg = _("More than one %(resource)s exists with "
-                    "the name '%(name_or_id)s'.")
-            raise exceptions.CommandError(
-                msg % {'resource': manager.resource_class.__name__.lower(),
-                       'name_or_id': name_or_id}
-            )
-        else:
-            pass
-
-    for resource in manager.list():
-        # short circuit and return the first match
-        if (resource.get('id') == name_or_id or
-                resource.get('name') == name_or_id):
-            return resource
-    else:
-        # we found no match, report back this error:
-        msg = _("Could not find resource %s") % name_or_id
-        raise exceptions.CommandError(msg)
-
-
-def format_dict(data):
-    """Return a formatted string of key value pairs
-
-    :param data: a dict
-    :rtype: a string formatted to key='value'
-    """
-
-    output = ""
-    for s in sorted(data):
-        output = output + s + "='" + six.text_type(data[s]) + "', "
-    return output[:-2]
-
-
-def format_list(data, separator=', '):
-    """Return a formatted strings
-
-    :param data: a list of strings
-    :param separator: the separator to use between strings (default: ', ')
-    :rtype: a string formatted based on separator
-    """
-
-    return separator.join(sorted(data))
-
-
-def format_list_of_dicts(data):
-    """Return a formatted string of key value pairs for each dict
-
-    :param data: a list of dicts
-    :rtype: a string formatted to key='value' with dicts separated by new line
-    """
-
-    return '\n'.join(format_dict(i) for i in data)
-
-
-def get_field(item, field):
-    try:
-        if isinstance(item, dict):
-            return item[field]
-        else:
-            return getattr(item, field)
-    except Exception:
-        msg = _("Resource doesn't have field %s") % field
-        raise exceptions.CommandError(msg)
-
-
-def get_item_properties(item, fields, mixed_case_fields=None, formatters=None):
-    """Return a tuple containing the item properties.
-
-    :param item: a single item resource (e.g. Server, Project, etc)
-    :param fields: tuple of strings with the desired field names
-    :param mixed_case_fields: tuple of field names to preserve case
-    :param formatters: dictionary mapping field names to callables
-       to format the values
-    """
-    if mixed_case_fields is None:
-        mixed_case_fields = []
-    if formatters is None:
-        formatters = {}
-
-    row = []
-
-    for field in fields:
-        if field in mixed_case_fields:
-            field_name = field.replace(' ', '_')
-        else:
-            field_name = field.lower().replace(' ', '_')
-        data = getattr(item, field_name, '')
-        if field in formatters:
-            row.append(formatters[field](data))
-        else:
-            row.append(data)
-    return tuple(row)
-
-
-def get_dict_properties(item, fields, mixed_case_fields=None, formatters=None):
-    """Return a tuple containing the item properties.
-
-    :param item: a single dict resource
-    :param fields: tuple of strings with the desired field names
-    :param mixed_case_fields: tuple of field names to preserve case
-    :param formatters: dictionary mapping field names to callables
-       to format the values
-    """
-    if mixed_case_fields is None:
-        mixed_case_fields = []
-    if formatters is None:
-        formatters = {}
-
-    row = []
-
-    for field in fields:
-        if field in mixed_case_fields:
-            field_name = field.replace(' ', '_')
-        else:
-            field_name = field.lower().replace(' ', '_')
-        data = item[field_name] if field_name in item else ''
-        if field in formatters:
-            row.append(formatters[field](data))
-        else:
-            row.append(data)
-    return tuple(row)
-
-
-def sort_items(items, sort_str):
-    """Sort items based on sort keys and sort directions given by sort_str.
-
-    :param items: a list or generator object of items
-    :param sort_str: a string defining the sort rules, the format is
-        '<key1>:[direction1],<key2>:[direction2]...', direction can be 'asc'
-        for ascending or 'desc' for descending, if direction is not given,
-        it's ascending by default
-    :return: sorted items
-    """
-    if not sort_str:
-        return items
-    # items may be a generator object, transform it to a list
-    items = list(items)
-    sort_keys = sort_str.strip().split(',')
-    for sort_key in reversed(sort_keys):
-        reverse = False
-        if ':' in sort_key:
-            sort_key, direction = sort_key.split(':', 1)
-            if not sort_key:
-                msg = _("empty string is not a valid sort key")
-                raise exceptions.CommandError(msg)
-            if direction not in ['asc', 'desc']:
-                if not direction:
-                    direction = "empty string"
-                msg = _("%(direction)s is not a valid sort direction for "
-                        "sort key %(sort_key)s, use asc or desc instead")
-                raise exceptions.CommandError(
-                    msg % {'direction': direction,
-                           'sort_key': sort_key}
-                )
-            if direction == 'desc':
-                reverse = True
-        items.sort(key=lambda item: get_field(item, sort_key),
-                   reverse=reverse)
-    return items
-
-
-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 get_client_class(api_name, version, version_map):
-    """Returns the client class for the requested API version
-
-    :param api_name: the name of the API, e.g. 'compute', 'image', etc
-    :param version: the requested API version
-    :param version_map: a dict of client classes keyed by version
-    :rtype: a client class for the requested API version
-    """
-    try:
-        client_path = version_map[str(version)]
-    except (KeyError, ValueError):
-        msg = _("Invalid %(api_name)s client version '%(version)s'. "
-                "must be one of: %(version_map)s")
-        raise exceptions.UnsupportedVersion(
-            msg % {'api_name': api_name,
-                   'version': version,
-                   'version_map': ', '.join(list(version_map.keys()))}
-        )
-
-    return importutils.import_class(client_path)
-
-
-def wait_for_status(status_f,
-                    res_id,
-                    status_field='status',
-                    success_status=['active'],
-                    error_status=['error'],
-                    sleep_time=5,
-                    callback=None):
-    """Wait for status change on a resource during a long-running operation
-
-    :param status_f: a status function that takes a single id argument
-    :param res_id: the resource id to watch
-    :param status_field: the status attribute in the returned resource object
-    :param success_status: a list of status strings for successful completion
-    :param error_status: a list of status strings for error
-    :param sleep_time: wait this long (seconds)
-    :param callback: called per sleep cycle, useful to display progress
-    :rtype: True on success
-    """
-    while True:
-        res = status_f(res_id)
-        status = getattr(res, status_field, '').lower()
-        if status in success_status:
-            retval = True
-            break
-        elif status in error_status:
-            retval = False
-            break
-        if callback:
-            progress = getattr(res, 'progress', None) or 0
-            callback(progress)
-        time.sleep(sleep_time)
-    return retval
-
-
-def wait_for_delete(manager,
-                    res_id,
-                    status_field='status',
-                    error_status=['error'],
-                    exception_name=['NotFound'],
-                    sleep_time=5,
-                    timeout=300,
-                    callback=None):
-    """Wait for resource deletion
-
-    :param manager: the manager from which we can get the resource
-    :param res_id: the resource id to watch
-    :param status_field: the status attribute in the returned resource object,
-        this is used to check for error states while the resource is being
-        deleted
-    :param error_status: a list of status strings for error
-    :param exception_name: a list of exception strings for deleted case
-    :param sleep_time: wait this long between checks (seconds)
-    :param timeout: check until this long (seconds)
-    :param callback: called per sleep cycle, useful to display progress; this
-        function is passed a progress value during each iteration of the wait
-        loop
-    :rtype: True on success, False if the resource has gone to error state or
-        the timeout has been reached
-    """
-    total_time = 0
-    while total_time < timeout:
-        try:
-            # might not be a bad idea to re-use find_resource here if it was
-            # a bit more friendly in the exceptions it raised so we could just
-            # handle a NotFound exception here without parsing the message
-            res = manager.get(res_id)
-        except Exception as ex:
-            if type(ex).__name__ in exception_name:
-                return True
-            raise
-
-        status = getattr(res, status_field, '').lower()
-        if status in error_status:
-            return False
-
-        if callback:
-            progress = getattr(res, 'progress', None) or 0
-            callback(progress)
-        time.sleep(sleep_time)
-        total_time += sleep_time
-
-    # if we got this far we've timed out
-    return False
-
-
-def get_effective_log_level():
-    """Returns the lowest logging level considered by logging handlers
-
-    Retrieve and return the smallest log level set among the root
-    logger's handlers (in case of multiple handlers).
-    """
-    root_log = logging.getLogger()
-    min_log_lvl = logging.CRITICAL
-    for handler in root_log.handlers:
-        min_log_lvl = min(min_log_lvl, handler.level)
-    return min_log_lvl
-
-
-def get_password(stdin, prompt=None, confirm=True):
-    message = prompt or "User Password:"
-    if hasattr(stdin, 'isatty') and stdin.isatty():
-        try:
-            while True:
-                first_pass = getpass.getpass(message)
-                if not confirm:
-                    return first_pass
-                second_pass = getpass.getpass("Repeat " + message)
-                if first_pass == second_pass:
-                    return first_pass
-                print("The passwords entered were not the same")
-        except EOFError:  # Ctl-D
-            raise exceptions.CommandError(_("Error reading password."))
-    raise exceptions.CommandError(_("There was a request to be prompted "
-                                    "for a password and a terminal was "
-                                    "not detected."))
-
-
-def read_blob_file_contents(blob_file):
-    try:
-        with open(blob_file) as file:
-            blob = file.read().strip()
-        return blob
-    except IOError:
-        msg = _("Error occurred trying to read from file %s")
-        raise exceptions.CommandError(msg % blob_file)
-
-
-def build_kwargs_dict(arg_name, value):
-    """Return a dictionary containing `arg_name` if `value` is set."""
-    kwargs = {}
-    if value:
-        kwargs[arg_name] = value
-    return kwargs
-
-
-def is_ascii(string):
-    try:
-        string.decode('ascii')
-        return True
-    except UnicodeDecodeError:
-        return False
+sys.stderr.write(
+    "WARNING: %s is deprecated and will be removed after Jun 2017. "
+    "Please use osc_lib.utils\n" % __name__
+)
diff --git a/requirements.txt b/requirements.txt
index 5d8c844a71..f3affbd37d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,6 +9,7 @@ cliff!=1.16.0,!=1.17.0,>=1.15.0 # Apache-2.0
 keystoneauth1>=2.1.0 # Apache-2.0
 openstacksdk>=0.8.6 # Apache-2.0
 os-client-config>=1.13.1 # Apache-2.0
+osc-lib>=0.1.0 # Apache-2.0
 oslo.config>=3.10.0 # Apache-2.0
 oslo.i18n>=2.1.0 # Apache-2.0
 oslo.utils>=3.11.0 # Apache-2.0