diff --git a/trove/common/auth.py b/trove/common/auth.py index 8e64a2bc0a..c9dc307495 100644 --- a/trove/common/auth.py +++ b/trove/common/auth.py @@ -17,6 +17,8 @@ import re import webob.exc import wsgi +from oslo.utils import strutils + from trove.common import exception from trove.openstack.common import log as logging from trove.common.i18n import _ @@ -59,7 +61,7 @@ class TenantBasedAuth(object): match_for_tenant = self.tenant_scoped_url.match(request.path_info) if (match_for_tenant and tenant_id == match_for_tenant.group('tenant_id')): - LOG.debug(logging.mask_password( + LOG.debug(strutils.mask_password( _("Authorized tenant '%(tenant_id)s' request: " "%(request)s") % {'tenant_id': tenant_id, 'request': request})) diff --git a/trove/common/utils.py b/trove/common/utils.py index 3fbc5c0851..4c98324e22 100644 --- a/trove/common/utils.py +++ b/trove/common/utils.py @@ -191,7 +191,11 @@ def poll_until(retriever, condition=lambda value: value, raise loopingcall.LoopingCallDone(retvalue=obj) if time_out is not None and time.time() - start_time > time_out: raise exception.PollTimeOut - lc = loopingcall.LoopingCall(f=poll_and_check).start(sleep_time, True) + + lc = loopingcall.FixedIntervalLoopingCall( + f=poll_and_check).start( + sleep_time, True) + return lc.wait() diff --git a/trove/extensions/mysql/service.py b/trove/extensions/mysql/service.py index cc7e9d3917..bad78e0528 100644 --- a/trove/extensions/mysql/service.py +++ b/trove/extensions/mysql/service.py @@ -15,6 +15,8 @@ import webob.exc +from oslo.utils import strutils + from trove.common import exception from trove.common import pagination from trove.common import wsgi @@ -82,8 +84,8 @@ class UserController(wsgi.Controller): def create(self, req, body, tenant_id, instance_id): """Creates a set of users.""" LOG.info(_("Creating users for instance '%s'") % instance_id) - LOG.info(_("req : '%s'\n\n") % logging.mask_password(req)) - LOG.info(_("body : '%s'\n\n") % logging.mask_password(body)) + LOG.info(_("req : '%s'\n\n") % strutils.mask_password(req)) + LOG.info(_("body : '%s'\n\n") % strutils.mask_password(body)) context = req.environ[wsgi.CONTEXT_KEY] users = body['users'] try: @@ -135,7 +137,7 @@ class UserController(wsgi.Controller): def update(self, req, body, tenant_id, instance_id, id): """Change attributes for one user.""" LOG.info(_("Updating user attributes for instance '%s'") % instance_id) - LOG.info(_("req : '%s'\n\n") % logging.mask_password(req)) + LOG.info(_("req : '%s'\n\n") % strutils.mask_password(req)) context = req.environ[wsgi.CONTEXT_KEY] id = correct_id_with_req(id, req) username, hostname = unquote_user_host(id) @@ -157,7 +159,7 @@ class UserController(wsgi.Controller): def update_all(self, req, body, tenant_id, instance_id): """Change the password of one or more users.""" LOG.info(_("Updating user passwords for instance '%s'") % instance_id) - LOG.info(_("req : '%s'\n\n") % logging.mask_password(req)) + LOG.info(_("req : '%s'\n\n") % strutils.mask_password(req)) context = req.environ[wsgi.CONTEXT_KEY] users = body['users'] model_users = [] diff --git a/trove/instance/service.py b/trove/instance/service.py index 542d7ce4b1..ff0eb6fbc6 100644 --- a/trove/instance/service.py +++ b/trove/instance/service.py @@ -15,6 +15,8 @@ import webob.exc +from oslo.utils import strutils + from trove.common import cfg from trove.common import exception from trove.common import pagination @@ -184,8 +186,8 @@ class InstanceController(wsgi.Controller): # TODO(hub-cap): turn this into middleware LOG.info(_LI("Creating a database instance for tenant '%s'"), tenant_id) - LOG.debug("req : '%s'\n\n", logging.mask_password(req)) - LOG.debug("body : '%s'\n\n", logging.mask_password(body)) + LOG.debug("req : '%s'\n\n", strutils.mask_password(req)) + LOG.debug("body : '%s'\n\n", strutils.mask_password(body)) context = req.environ[wsgi.CONTEXT_KEY] datastore_args = body['instance'].get('datastore', {}) datastore, datastore_version = ( @@ -273,8 +275,8 @@ class InstanceController(wsgi.Controller): Updates the instance to set or unset one or more attributes. """ LOG.info(_LI("Editing instance for tenant id %s."), tenant_id) - LOG.debug("req: %s", logging.mask_password(req)) - LOG.debug("body: %s", logging.mask_password(body)) + LOG.debug("req: %s", strutils.mask_password(req)) + LOG.debug("body: %s", strutils.mask_password(body)) context = req.environ[wsgi.CONTEXT_KEY] instance = models.Instance.load(context, id) diff --git a/trove/openstack/common/__init__.py b/trove/openstack/common/__init__.py index d1223eaf76..e69de29bb2 100644 --- a/trove/openstack/common/__init__.py +++ b/trove/openstack/common/__init__.py @@ -1,17 +0,0 @@ -# -# 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 six - - -six.add_move(six.MovedModule('mox', 'mox', 'mox3.mox')) diff --git a/trove/openstack/common/_i18n.py b/trove/openstack/common/_i18n.py new file mode 100644 index 0000000000..f929dde9f9 --- /dev/null +++ b/trove/openstack/common/_i18n.py @@ -0,0 +1,45 @@ +# 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. + +"""oslo.i18n integration module. + +See http://docs.openstack.org/developer/oslo.i18n/usage.html + +""" + +try: + import oslo.i18n + + # NOTE(dhellmann): This reference to o-s-l-o will be replaced by the + # application name when this module is synced into the separate + # repository. It is OK to have more than one translation function + # using the same domain, since there will still only be one message + # catalog. + _translators = oslo.i18n.TranslatorFactory(domain='trove') + + # The primary translation function using the well-known name "_" + _ = _translators.primary + + # Translators for log levels. + # + # The abbreviated names are meant to reflect the usual use of a short + # name like '_'. The "L" is for "log" and the other letter comes from + # the level. + _LI = _translators.log_info + _LW = _translators.log_warning + _LE = _translators.log_error + _LC = _translators.log_critical +except ImportError: + # NOTE(dims): Support for cases where a project wants to use + # code from trove-incubator, but is not ready to be internationalized + # (like tempest) + _ = _LI = _LW = _LE = _LC = lambda x: x diff --git a/trove/openstack/common/context.py b/trove/openstack/common/context.py index 182b044365..168989004c 100644 --- a/trove/openstack/common/context.py +++ b/trove/openstack/common/context.py @@ -25,7 +25,7 @@ import uuid def generate_request_id(): - return 'req-%s' % str(uuid.uuid4()) + return b'req-' + str(uuid.uuid4()).encode('ascii') class RequestContext(object): @@ -77,6 +77,21 @@ class RequestContext(object): 'instance_uuid': self.instance_uuid, 'user_identity': user_idt} + @classmethod + def from_dict(cls, ctx): + return cls( + auth_token=ctx.get("auth_token"), + user=ctx.get("user"), + tenant=ctx.get("tenant"), + domain=ctx.get("domain"), + user_domain=ctx.get("user_domain"), + project_domain=ctx.get("project_domain"), + is_admin=ctx.get("is_admin", False), + read_only=ctx.get("read_only", False), + show_deleted=ctx.get("show_deleted", False), + request_id=ctx.get("request_id"), + instance_uuid=ctx.get("instance_uuid")) + def get_admin_context(show_deleted=False): context = RequestContext(None, @@ -98,3 +113,10 @@ def get_context_from_function_and_args(function, args, kwargs): return arg return None + + +def is_user_context(context): + """Indicates if the request context is a normal user.""" + if not context or context.is_admin: + return False + return context.user_id and context.project_id diff --git a/trove/openstack/common/eventlet_backdoor.py b/trove/openstack/common/eventlet_backdoor.py index b3f55fa3c9..61550a6ec1 100644 --- a/trove/openstack/common/eventlet_backdoor.py +++ b/trove/openstack/common/eventlet_backdoor.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright (c) 2012 OpenStack Foundation. # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. @@ -18,6 +16,7 @@ from __future__ import print_function +import copy import errno import gc import os @@ -31,7 +30,7 @@ import eventlet.backdoor import greenlet from oslo.config import cfg -from trove.openstack.common.gettextutils import _ # noqa +from trove.openstack.common._i18n import _LI from trove.openstack.common import log as logging help_for_backdoor_port = ( @@ -43,7 +42,6 @@ help_for_backdoor_port = ( "chosen port is displayed in the service's log file.") eventlet_backdoor_opts = [ cfg.StrOpt('backdoor_port', - default=None, help="Enable eventlet backdoor. %s" % help_for_backdoor_port) ] @@ -52,6 +50,12 @@ CONF.register_opts(eventlet_backdoor_opts) LOG = logging.getLogger(__name__) +def list_opts(): + """Entry point for oslo.config-generator. + """ + return [(None, copy.deepcopy(eventlet_backdoor_opts))] + + class EventletBackdoorConfigValueError(Exception): def __init__(self, port_range, help_msg, ex): msg = ('Invalid backdoor_port configuration %(range)s: %(ex)s. ' @@ -66,7 +70,7 @@ def _dont_use_this(): def _find_objects(t): - return filter(lambda o: isinstance(o, t), gc.get_objects()) + return [o for o in gc.get_objects() if isinstance(o, t)] def _print_greenthreads(): @@ -139,8 +143,10 @@ def initialize_if_enabled(): # In the case of backdoor port being zero, a port number is assigned by # listen(). In any case, pull the port number out here. port = sock.getsockname()[1] - LOG.info(_('Eventlet backdoor listening on %(port)s for process %(pid)d') % - {'port': port, 'pid': os.getpid()}) + LOG.info( + _LI('Eventlet backdoor listening on %(port)s for process %(pid)d') % + {'port': port, 'pid': os.getpid()} + ) eventlet.spawn_n(eventlet.backdoor.backdoor_server, sock, locals=backdoor_locals) return port diff --git a/trove/openstack/common/log.py b/trove/openstack/common/log.py index 813676ffab..180cfcf4f7 100644 --- a/trove/openstack/common/log.py +++ b/trove/openstack/common/log.py @@ -27,28 +27,27 @@ It also allows setting of formatting information through conf. """ +import copy import inspect import itertools import logging import logging.config import logging.handlers import os +import socket import sys import traceback from oslo.config import cfg +from oslo.serialization import jsonutils +from oslo.utils import importutils import six from six import moves _PY26 = sys.version_info[0:2] == (2, 6) -from trove.openstack.common.gettextutils import _ -from trove.openstack.common import importutils -from trove.openstack.common import jsonutils +from trove.openstack.common._i18n import _ from trove.openstack.common import local -# NOTE(flaper87): Pls, remove when graduating this module -# from the incubator. -from trove.openstack.common.strutils import mask_password # noqa _DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" @@ -126,7 +125,9 @@ DEFAULT_LOG_LEVELS = ['amqp=WARN', 'amqplib=WARN', 'boto=WARN', 'qpid=WARN', 'sqlalchemy=WARN', 'suds=INFO', 'oslo.messaging=INFO', 'iso8601=WARN', 'requests.packages.urllib3.connectionpool=WARN', - 'urllib3.connectionpool=WARN', 'websocket=WARN'] + 'urllib3.connectionpool=WARN', 'websocket=WARN', + "keystonemiddleware=WARN", "routes.middleware=WARN", + "stevedore=WARN"] log_opts = [ cfg.StrOpt('logging_context_format_string', @@ -174,6 +175,16 @@ CONF.register_cli_opts(logging_cli_opts) CONF.register_opts(generic_log_opts) CONF.register_opts(log_opts) + +def list_opts(): + """Entry point for oslo.config-generator.""" + return [(None, copy.deepcopy(common_cli_opts)), + (None, copy.deepcopy(logging_cli_opts)), + (None, copy.deepcopy(generic_log_opts)), + (None, copy.deepcopy(log_opts)), + ] + + # our new audit level # NOTE(jkoelker) Since we synthesized an audit level, make the logging # module aware of it so it acts like other levels. @@ -300,11 +311,10 @@ class ContextAdapter(BaseLoggerAdapter): self.warn(stdmsg, *args, **kwargs) def process(self, msg, kwargs): - # NOTE(mrodden): catch any Message/other object and - # coerce to unicode before they can get - # to the python logging and possibly - # cause string encoding trouble - if not isinstance(msg, six.string_types): + # NOTE(jecarey): If msg is not unicode, coerce it into unicode + # before it can get to the python logging and + # possibly cause string encoding trouble + if not isinstance(msg, six.text_type): msg = six.text_type(msg) if 'extra' not in kwargs: @@ -429,12 +439,12 @@ def set_defaults(logging_context_format_string=None, # later in a backwards in-compatible change if default_log_levels is not None: cfg.set_defaults( - log_opts, - default_log_levels=default_log_levels) + log_opts, + default_log_levels=default_log_levels) if logging_context_format_string is not None: cfg.set_defaults( - log_opts, - logging_context_format_string=logging_context_format_string) + log_opts, + logging_context_format_string=logging_context_format_string) def _find_facility_from_conf(): @@ -483,18 +493,6 @@ def _setup_logging_from_conf(project, version): for handler in log_root.handlers: log_root.removeHandler(handler) - if CONF.use_syslog: - facility = _find_facility_from_conf() - # TODO(bogdando) use the format provided by RFCSysLogHandler - # after existing syslog format deprecation in J - if CONF.use_syslog_rfc_format: - syslog = RFCSysLogHandler(address='/dev/log', - facility=facility) - else: - syslog = logging.handlers.SysLogHandler(address='/dev/log', - facility=facility) - log_root.addHandler(syslog) - logpath = _get_log_file_path() if logpath: filelog = logging.handlers.WatchedFileHandler(logpath) @@ -511,14 +509,9 @@ def _setup_logging_from_conf(project, version): log_root.addHandler(streamlog) if CONF.publish_errors: - try: - handler = importutils.import_object( - "trove.openstack.common.log_handler.PublishErrorsHandler", - logging.ERROR) - except ImportError: - handler = importutils.import_object( - "oslo.messaging.notify.log_handler.PublishErrorsHandler", - logging.ERROR) + handler = importutils.import_object( + "oslo.messaging.notify.log_handler.PublishErrorsHandler", + logging.ERROR) log_root.addHandler(handler) datefmt = CONF.log_date_format @@ -553,6 +546,22 @@ def _setup_logging_from_conf(project, version): else: logger.setLevel(level_name) + if CONF.use_syslog: + try: + facility = _find_facility_from_conf() + # TODO(bogdando) use the format provided by RFCSysLogHandler + # after existing syslog format deprecation in J + if CONF.use_syslog_rfc_format: + syslog = RFCSysLogHandler(address='/dev/log', + facility=facility) + else: + syslog = logging.handlers.SysLogHandler(address='/dev/log', + facility=facility) + log_root.addHandler(syslog) + except socket.error: + log_root.error('Unable to add syslog handler. Verify that syslog ' + 'is running.') + _loggers = {} @@ -622,6 +631,12 @@ class ContextFormatter(logging.Formatter): def format(self, record): """Uses contextstring if request_id is set, otherwise default.""" + # NOTE(jecarey): If msg is not unicode, coerce it into unicode + # before it can get to the python logging and + # possibly cause string encoding trouble + if not isinstance(record.msg, six.text_type): + record.msg = six.text_type(record.msg) + # store project info record.project = self.project record.version = self.version diff --git a/trove/openstack/common/loopingcall.py b/trove/openstack/common/loopingcall.py index 0e45968614..a8cd4b71ca 100644 --- a/trove/openstack/common/loopingcall.py +++ b/trove/openstack/common/loopingcall.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # Copyright 2011 Justin Santa Barbara @@ -18,31 +16,36 @@ # under the License. import sys +import time from eventlet import event from eventlet import greenthread -from trove.openstack.common.gettextutils import _ # noqa +from trove.openstack.common._i18n import _LE, _LW from trove.openstack.common import log as logging -from trove.openstack.common import timeutils LOG = logging.getLogger(__name__) +# NOTE(zyluo): This lambda function was declared to avoid mocking collisions +# with time.time() called in the standard logging module +# during unittests. +_ts = lambda: time.time() + class LoopingCallDone(Exception): - """Exception to break out and stop a LoopingCall. + """Exception to break out and stop a LoopingCallBase. - The poll-function passed to LoopingCall can raise this exception to + The poll-function passed to LoopingCallBase can raise this exception to break out of the loop normally. This is somewhat analogous to StopIteration. An optional return-value can be included as the argument to the exception; - this return-value will be returned by LoopingCall.wait() + this return-value will be returned by LoopingCallBase.wait() """ def __init__(self, retvalue=True): - """:param retvalue: Value that LoopingCall.wait() should return.""" + """:param retvalue: Value that LoopingCallBase.wait() should return.""" self.retvalue = retvalue @@ -74,21 +77,22 @@ class FixedIntervalLoopingCall(LoopingCallBase): try: while self._running: - start = timeutils.utcnow() + start = _ts() self.f(*self.args, **self.kw) - end = timeutils.utcnow() + end = _ts() if not self._running: break - delay = interval - timeutils.delta_seconds(start, end) - if delay <= 0: - LOG.warn(_('task run outlasted interval by %s sec') % - -delay) - greenthread.sleep(delay if delay > 0 else 0) + delay = end - start - interval + if delay > 0: + LOG.warn(_LW('task %(func_name)s run outlasted ' + 'interval by %(delay).2f sec'), + {'func_name': repr(self.f), 'delay': delay}) + greenthread.sleep(-delay if delay < 0 else 0) except LoopingCallDone as e: self.stop() done.send(e.retvalue) except Exception: - LOG.exception(_('in fixed duration looping call')) + LOG.exception(_LE('in fixed duration looping call')) done.send_exception(*sys.exc_info()) return else: @@ -100,11 +104,6 @@ class FixedIntervalLoopingCall(LoopingCallBase): return self.done -# TODO(mikal): this class name is deprecated in Havana and should be removed -# in the I release -LoopingCall = FixedIntervalLoopingCall - - class DynamicLoopingCall(LoopingCallBase): """A looping call which sleeps until the next known event. @@ -128,14 +127,15 @@ class DynamicLoopingCall(LoopingCallBase): if periodic_interval_max is not None: idle = min(idle, periodic_interval_max) - LOG.debug(_('Dynamic looping call sleeping for %.02f ' - 'seconds'), idle) + LOG.debug('Dynamic looping call %(func_name)s sleeping ' + 'for %(idle).02f seconds', + {'func_name': repr(self.f), 'idle': idle}) greenthread.sleep(idle) except LoopingCallDone as e: self.stop() done.send(e.retvalue) except Exception: - LOG.exception(_('in dynamic looping call')) + LOG.exception(_LE('in dynamic looping call')) done.send_exception(*sys.exc_info()) return else: diff --git a/trove/openstack/common/service.py b/trove/openstack/common/service.py index d2b5820fbb..d67ccf5570 100644 --- a/trove/openstack/common/service.py +++ b/trove/openstack/common/service.py @@ -38,7 +38,7 @@ from eventlet import event from oslo.config import cfg from trove.openstack.common import eventlet_backdoor -from trove.openstack.common.gettextutils import _LE, _LI, _LW +from trove.openstack.common._i18n import _LE, _LI, _LW from trove.openstack.common import log as logging from trove.openstack.common import systemd from trove.openstack.common import threadgroup diff --git a/trove/openstack/common/threadgroup.py b/trove/openstack/common/threadgroup.py index c5b6aa04f6..4ffbc8c95a 100644 --- a/trove/openstack/common/threadgroup.py +++ b/trove/openstack/common/threadgroup.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -13,10 +11,10 @@ # 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 threading import eventlet from eventlet import greenpool -from eventlet import greenthread from trove.openstack.common import log as logging from trove.openstack.common import loopingcall @@ -48,9 +46,12 @@ class Thread(object): def wait(self): return self.thread.wait() + def link(self, func, *args, **kwargs): + self.thread.link(func, *args, **kwargs) + class ThreadGroup(object): - """The point of the ThreadGroup classis to: + """The point of the ThreadGroup class is to: * keep track of timers and greenthreads (making it easier to stop them when need be). @@ -79,21 +80,28 @@ class ThreadGroup(object): gt = self.pool.spawn(callback, *args, **kwargs) th = Thread(gt, self) self.threads.append(th) + return th def thread_done(self, thread): self.threads.remove(thread) - def stop(self): - current = greenthread.getcurrent() - for x in self.threads: + def _stop_threads(self): + current = threading.current_thread() + + # Iterate over a copy of self.threads so thread_done doesn't + # modify the list while we're iterating + for x in self.threads[:]: if x is current: # don't kill the current thread. continue try: x.stop() + except eventlet.greenlet.GreenletExit: + pass except Exception as ex: LOG.exception(ex) + def stop_timers(self): for x in self.timers: try: x.stop() @@ -101,6 +109,23 @@ class ThreadGroup(object): LOG.exception(ex) self.timers = [] + def stop(self, graceful=False): + """stop function has the option of graceful=True/False. + + * In case of graceful=True, wait for all threads to be finished. + Never kill threads. + * In case of graceful=False, kill threads immediately. + """ + self.stop_timers() + if graceful: + # In case of graceful=True, wait for all threads to be + # finished, never kill threads + self.wait() + else: + # In case of graceful=False(Default), kill threads + # immediately + self._stop_threads() + def wait(self): for x in self.timers: try: @@ -109,8 +134,11 @@ class ThreadGroup(object): pass except Exception as ex: LOG.exception(ex) - current = greenthread.getcurrent() - for x in self.threads: + current = threading.current_thread() + + # Iterate over a copy of self.threads so thread_done doesn't + # modify the list while we're iterating + for x in self.threads[:]: if x is current: continue try: diff --git a/trove/openstack/common/versionutils.py b/trove/openstack/common/versionutils.py index d1826e692c..ca0b8036b8 100644 --- a/trove/openstack/common/versionutils.py +++ b/trove/openstack/common/versionutils.py @@ -18,9 +18,12 @@ Helpers for comparing version strings. """ import functools -import pkg_resources +import inspect -from trove.openstack.common.gettextutils import _ +import pkg_resources +import six + +from trove.openstack.common._i18n import _ from trove.openstack.common import log as logging @@ -52,18 +55,36 @@ class deprecated(object): >>> @deprecated(as_of=deprecated.ICEHOUSE, remove_in=+1) ... def c(): pass + 4. Specifying the deprecated functionality will not be removed: + >>> @deprecated(as_of=deprecated.ICEHOUSE, remove_in=0) + ... def d(): pass + + 5. Specifying a replacement, deprecated functionality will not be removed: + >>> @deprecated(as_of=deprecated.ICEHOUSE, in_favor_of='f()', remove_in=0) + ... def e(): pass + """ + # NOTE(morganfainberg): Bexar is used for unit test purposes, it is + # expected we maintain a gap between Bexar and Folsom in this list. + BEXAR = 'B' FOLSOM = 'F' GRIZZLY = 'G' HAVANA = 'H' ICEHOUSE = 'I' + JUNO = 'J' + KILO = 'K' _RELEASES = { + # NOTE(morganfainberg): Bexar is used for unit test purposes, it is + # expected we maintain a gap between Bexar and Folsom in this list. + 'B': 'Bexar', 'F': 'Folsom', 'G': 'Grizzly', 'H': 'Havana', 'I': 'Icehouse', + 'J': 'Juno', + 'K': 'Kilo', } _deprecated_msg_with_alternative = _( @@ -74,6 +95,12 @@ class deprecated(object): '%(what)s is deprecated as of %(as_of)s and may be ' 'removed in %(remove_in)s. It will not be superseded.') + _deprecated_msg_with_alternative_no_removal = _( + '%(what)s is deprecated as of %(as_of)s in favor of %(in_favor_of)s.') + + _deprecated_msg_with_no_alternative_no_removal = _( + '%(what)s is deprecated as of %(as_of)s. It will not be superseded.') + def __init__(self, as_of, in_favor_of=None, remove_in=2, what=None): """Initialize decorator @@ -91,16 +118,34 @@ class deprecated(object): self.remove_in = remove_in self.what = what - def __call__(self, func): + def __call__(self, func_or_cls): if not self.what: - self.what = func.__name__ + '()' + self.what = func_or_cls.__name__ + '()' + msg, details = self._build_message() - @functools.wraps(func) - def wrapped(*args, **kwargs): - msg, details = self._build_message() - LOG.deprecated(msg, details) - return func(*args, **kwargs) - return wrapped + if inspect.isfunction(func_or_cls): + + @six.wraps(func_or_cls) + def wrapped(*args, **kwargs): + LOG.deprecated(msg, details) + return func_or_cls(*args, **kwargs) + return wrapped + elif inspect.isclass(func_or_cls): + orig_init = func_or_cls.__init__ + + # TODO(tsufiev): change `functools` module to `six` as + # soon as six 1.7.4 (with fix for passing `assigned` + # argument to underlying `functools.wraps`) is released + # and added to the trove-incubator requrements + @functools.wraps(orig_init, assigned=('__name__', '__doc__')) + def new_init(self, *args, **kwargs): + LOG.deprecated(msg, details) + orig_init(self, *args, **kwargs) + func_or_cls.__init__ = new_init + return func_or_cls + else: + raise TypeError('deprecated can be used only with functions or ' + 'classes') def _get_safe_to_remove_release(self, release): # TODO(dstanek): this method will have to be reimplemented once @@ -119,9 +164,19 @@ class deprecated(object): if self.in_favor_of: details['in_favor_of'] = self.in_favor_of - msg = self._deprecated_msg_with_alternative + if self.remove_in > 0: + msg = self._deprecated_msg_with_alternative + else: + # There are no plans to remove this function, but it is + # now deprecated. + msg = self._deprecated_msg_with_alternative_no_removal else: - msg = self._deprecated_msg_no_alternative + if self.remove_in > 0: + msg = self._deprecated_msg_no_alternative + else: + # There are no plans to remove this function, but it is + # now deprecated. + msg = self._deprecated_msg_with_no_alternative_no_removal return msg, details