From 7dae430e8111ff8d2380e31ba1306fd9bee0316b Mon Sep 17 00:00:00 2001 From: Eric Harney Date: Wed, 17 Jun 2015 16:00:35 -0400 Subject: [PATCH] Sync 'report' from oslo-incubator Needed to fix guru meditation report for Windows. We landed an outdated 'report' module recently. Add report.[generators,models,views] to openstack-common.conf. Oslo source: c4c7dd28 Updated from global requirements Closes-Bug: #1286528 Change-Id: Ib33542718176241663c0e61605acbf4e941e2ca9 --- cinder/openstack/common/report/__init__.py | 2 +- .../common/report/generators/__init__.py | 2 +- .../common/report/generators/conf.py | 10 +-- .../common/report/generators/process.py | 38 ++++++++++ .../common/report/generators/threading.py | 35 ++++++--- .../common/report/generators/version.py | 28 +++++-- .../common/report/guru_meditation_report.py | 74 +++++++++++++++---- .../common/report/models/__init__.py | 2 +- cinder/openstack/common/report/models/base.py | 68 ++++++++++++++--- cinder/openstack/common/report/models/conf.py | 28 ++++--- .../openstack/common/report/models/process.py | 62 ++++++++++++++++ .../common/report/models/threading.py | 12 +-- .../openstack/common/report/models/version.py | 8 +- .../report/models/with_default_views.py | 31 ++++---- cinder/openstack/common/report/report.py | 20 +++-- cinder/openstack/common/report/utils.py | 2 +- .../openstack/common/report/views/__init__.py | 2 +- .../common/report/views/jinja_view.py | 12 +++ .../common/report/views/json/__init__.py | 2 +- .../common/report/views/json/generic.py | 10 +-- .../common/report/views/text/__init__.py | 2 +- .../common/report/views/text/generic.py | 6 +- .../common/report/views/text/header.py | 1 - .../common/report/views/text/process.py | 38 ++++++++++ .../common/report/views/text/threading.py | 4 +- .../common/report/views/xml/__init__.py | 2 +- .../common/report/views/xml/generic.py | 12 +-- openstack-common.conf | 6 ++ requirements.txt | 1 + 29 files changed, 403 insertions(+), 117 deletions(-) create mode 100644 cinder/openstack/common/report/generators/process.py create mode 100644 cinder/openstack/common/report/models/process.py create mode 100644 cinder/openstack/common/report/views/text/process.py diff --git a/cinder/openstack/common/report/__init__.py b/cinder/openstack/common/report/__init__.py index b296462c0..35390ecd3 100644 --- a/cinder/openstack/common/report/__init__.py +++ b/cinder/openstack/common/report/__init__.py @@ -22,4 +22,4 @@ is composed of one or more report sections which contain generators which generate data models ( :class:`openstack.common.report.models.base.ReportModels` ), which are then serialized by views. -""" \ No newline at end of file +""" diff --git a/cinder/openstack/common/report/generators/__init__.py b/cinder/openstack/common/report/generators/__init__.py index 26474e3ba..68473f22d 100644 --- a/cinder/openstack/common/report/generators/__init__.py +++ b/cinder/openstack/common/report/generators/__init__.py @@ -18,4 +18,4 @@ This module defines classes for generating data models ( :class:`openstack.common.report.models.base.ReportModel` ). A generator is any object which is callable with no parameters and returns a data model. -""" \ No newline at end of file +""" diff --git a/cinder/openstack/common/report/generators/conf.py b/cinder/openstack/common/report/generators/conf.py index 31ccaecbb..c6a916bfe 100644 --- a/cinder/openstack/common/report/generators/conf.py +++ b/cinder/openstack/common/report/generators/conf.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -"""Provides Openstack config generators +"""Provides OpenStack config generators This module defines a class for configuration generators for generating the model in @@ -21,20 +21,20 @@ generators for generating the model in from oslo_config import cfg -import cinder.openstack.common.report.models.conf as cm +from cinder.openstack.common.report.models import conf as cm class ConfigReportGenerator(object): """A Configuration Data Generator This generator returns - :class:`openstack.common.report.models.conf.ConfigModel` , + :class:`openstack.common.report.models.conf.ConfigModel`, by default using the configuration options stored in :attr:`oslo_config.cfg.CONF`, which is where - Openstack stores everything. + OpenStack stores everything. :param cnf: the configuration option object - :type cnf: :class:`oslo.config.cfg.ConfigOpts` + :type cnf: :class:`oslo_config.cfg.ConfigOpts` """ def __init__(self, cnf=cfg.CONF): diff --git a/cinder/openstack/common/report/generators/process.py b/cinder/openstack/common/report/generators/process.py new file mode 100644 index 000000000..57afb7a69 --- /dev/null +++ b/cinder/openstack/common/report/generators/process.py @@ -0,0 +1,38 @@ +# Copyright 2014 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Provides process-data generators + +This modules defines a class for generating +process data by way of the psutil package. +""" + +import os + +import psutil + +from cinder.openstack.common.report.models import process as pm + + +class ProcessReportGenerator(object): + """A Process Data Generator + + This generator returns a + :class:`openstack.common.report.models.process.ProcessModel` + based on the current process (which will also include + all subprocesses, recursively) using the :class:`psutil.Process` class`. + """ + + def __call__(self): + return pm.ProcessModel(psutil.Process(os.getpid())) diff --git a/cinder/openstack/common/report/generators/threading.py b/cinder/openstack/common/report/generators/threading.py index 4f803e4b7..10c07503f 100644 --- a/cinder/openstack/common/report/generators/threading.py +++ b/cinder/openstack/common/report/generators/threading.py @@ -19,14 +19,17 @@ generators for generating the models in :mod:`openstack.common.report.models.threading`. """ +from __future__ import absolute_import + import sys +import threading import greenlet -import cinder.openstack.common.report.models.threading as tm +from cinder.openstack.common.report.models import threading as tm from cinder.openstack.common.report.models import with_default_views as mwdv -import cinder.openstack.common.report.utils as rutils -import cinder.openstack.common.report.views.text.generic as text_views +from cinder.openstack.common.report import utils as rutils +from cinder.openstack.common.report.views.text import generic as text_views class ThreadReportGenerator(object): @@ -35,17 +38,28 @@ class ThreadReportGenerator(object): This generator returns a collection of :class:`openstack.common.report.models.threading.ThreadModel` objects by introspecting the current python state using - :func:`sys._current_frames()` . + :func:`sys._current_frames()` . Its constructor may optionally + be passed a frame object. This frame object will be interpreted + as the actual stack trace for the current thread, and, come generation + time, will be used to replace the stack trace of the thread in which + this code is running. """ + def __init__(self, curr_thread_traceback=None): + self.traceback = curr_thread_traceback + def __call__(self): - threadModels = [ - tm.ThreadModel(thread_id, stack) + threadModels = dict( + (thread_id, tm.ThreadModel(thread_id, stack)) for thread_id, stack in sys._current_frames().items() - ] + ) + + if self.traceback is not None: + curr_thread_id = threading.current_thread().ident + threadModels[curr_thread_id] = tm.ThreadModel(curr_thread_id, + self.traceback) - thread_pairs = dict(zip(range(len(threadModels)), threadModels)) - return mwdv.ModelWithDefaultViews(thread_pairs, + return mwdv.ModelWithDefaultViews(threadModels, text_view=text_views.MultiView()) @@ -68,6 +82,5 @@ class GreenThreadReportGenerator(object): for gr in rutils._find_objects(greenlet.greenlet) ] - thread_pairs = dict(zip(range(len(threadModels)), threadModels)) - return mwdv.ModelWithDefaultViews(thread_pairs, + return mwdv.ModelWithDefaultViews(threadModels, text_view=text_views.MultiView()) diff --git a/cinder/openstack/common/report/generators/version.py b/cinder/openstack/common/report/generators/version.py index 05936fc74..77e1b3b80 100644 --- a/cinder/openstack/common/report/generators/version.py +++ b/cinder/openstack/common/report/generators/version.py @@ -12,15 +12,15 @@ # License for the specific language governing permissions and limitations # under the License. -"""Provides Openstack version generators +"""Provides OpenStack version generators -This module defines a class for Openstack +This module defines a class for OpenStack version and package information generators for generating the model in :mod:`openstack.common.report.models.version`. """ -import cinder.openstack.common.report.models.version as vm +from cinder.openstack.common.report.models import version as vm class PackageReportGenerator(object): @@ -40,7 +40,21 @@ class PackageReportGenerator(object): self.version_obj = version_obj def __call__(self): - return vm.PackageModel( - self.version_obj.vendor_string(), - self.version_obj.product_string(), - self.version_obj.version_string_with_package()) + if hasattr(self.version_obj, "vendor_string"): + vendor_string = self.version_obj.vendor_string() + else: + vendor_string = None + + if hasattr(self.version_obj, "product_string"): + product_string = self.version_obj.product_string() + else: + product_string = None + + if hasattr(self.version_obj, "version_string_with_package"): + version_string_with_package = self.version_obj.\ + version_string_with_package() + else: + version_string_with_package = None + + return vm.PackageModel(vendor_string, product_string, + version_string_with_package) diff --git a/cinder/openstack/common/report/guru_meditation_report.py b/cinder/openstack/common/report/guru_meditation_report.py index 9693f9be7..dd8af3924 100644 --- a/cinder/openstack/common/report/guru_meditation_report.py +++ b/cinder/openstack/common/report/guru_meditation_report.py @@ -14,7 +14,8 @@ """Provides Guru Meditation Report -This module defines the actual OpenStack Guru Meditation Report class. +This module defines the actual OpenStack Guru Meditation +Report class. This can be used in the OpenStack command definition files. For example, in a nova command module (under nova/cmd): @@ -50,10 +51,15 @@ where stderr is logged for that given service. from __future__ import print_function +import inspect +import os import signal import sys +from oslo_utils import timeutils + from cinder.openstack.common.report.generators import conf as cgen +from cinder.openstack.common.report.generators import process as prgen from cinder.openstack.common.report.generators import threading as tgen from cinder.openstack.common.report.generators import version as pgen from cinder.openstack.common.report import report @@ -73,8 +79,11 @@ class GuruMeditation(object): MRO is correct. """ - def __init__(self, version_obj, *args, **kwargs): + timestamp_fmt = "%Y%m%d%H%M%S" + + def __init__(self, version_obj, sig_handler_tb=None, *args, **kwargs): self.version_obj = version_obj + self.traceback = sig_handler_tb super(GuruMeditation, self).__init__(*args, **kwargs) self.start_section_index = len(self.sections) @@ -96,37 +105,69 @@ class GuruMeditation(object): cls.persistent_sections = [[section_title, generator]] @classmethod - def setup_autorun(cls, version, signum=signal.SIGUSR1): + def setup_autorun(cls, version, service_name=None, + log_dir=None, signum=None): """Set Up Auto-Run This method sets up the Guru Meditation Report to automatically - get dumped to stderr when the given signal is received. + get dumped to stderr or a file in a given dir when the given signal + is received. :param version: the version object for the current product + :param service_name: this program name used to construct logfile name + :param logdir: path to a log directory where to create a file :param signum: the signal to associate with running the report """ - signal.signal(signum, lambda *args: cls.handle_signal(version, *args)) + if not signum and hasattr(signal, 'SIGUSR1'): + # SIGUSR1 is not supported on all platforms + signum = signal.SIGUSR1 + + if signum: + signal.signal(signum, + lambda sn, tb: cls.handle_signal( + version, service_name, log_dir, tb)) @classmethod - def handle_signal(cls, version, *args): + def handle_signal(cls, version, service_name, log_dir, traceback): """The Signal Handler This method (indirectly) handles receiving a registered signal and - dumping the Guru Meditation Report to stderr. This method is designed - to be curried into a proper signal handler by currying out the version + dumping the Guru Meditation Report to stderr or a file in a given dir. + If service name and log dir are not None, the report will be dumped to + a file named $service_name_gurumeditation_$current_time in the log_dir + directory. + This method is designed to be curried into a proper signal handler by + currying out the version parameter. :param version: the version object for the current product + :param service_name: this program name used to construct logfile name + :param logdir: path to a log directory where to create a file + :param traceback: the traceback provided to the signal handler """ try: - res = cls(version).run() + res = cls(version, traceback).run() except Exception: print("Unable to run Guru Meditation Report!", file=sys.stderr) else: - print(res, file=sys.stderr) + if log_dir: + service_name = service_name or os.path.basename( + inspect.stack()[-1][1]) + filename = "%s_gurumeditation_%s" % ( + service_name, timeutils.utcnow().strftime( + cls.timestamp_fmt)) + filepath = os.path.join(log_dir, filename) + try: + with open(filepath, "w") as dumpfile: + dumpfile.write(res) + except Exception: + print("Unable to dump Guru Meditation Report to file %s" % + (filepath,), file=sys.stderr) + else: + print(res, file=sys.stderr) def _readd_sections(self): del self.sections[self.start_section_index:] @@ -135,11 +176,14 @@ class GuruMeditation(object): pgen.PackageReportGenerator(self.version_obj)) self.add_section('Threads', - tgen.ThreadReportGenerator()) + tgen.ThreadReportGenerator(self.traceback)) self.add_section('Green Threads', tgen.GreenThreadReportGenerator()) + self.add_section('Processes', + prgen.ProcessReportGenerator()) + self.add_section('Configuration', cgen.ConfigReportGenerator()) @@ -169,11 +213,15 @@ class TextGuruMeditation(GuruMeditation, report.TextReport): - Green Threads List + - Process List + - Configuration Options :param version_obj: the version object for the current product + :param traceback: an (optional) frame object providing the actual + traceback for the current thread """ - def __init__(self, version_obj): - super(TextGuruMeditation, self).__init__(version_obj, + def __init__(self, version_obj, traceback=None): + super(TextGuruMeditation, self).__init__(version_obj, traceback, 'Guru Meditation') diff --git a/cinder/openstack/common/report/models/__init__.py b/cinder/openstack/common/report/models/__init__.py index 8430fc45f..7bfed3d88 100644 --- a/cinder/openstack/common/report/models/__init__.py +++ b/cinder/openstack/common/report/models/__init__.py @@ -17,4 +17,4 @@ This module provides both the base data model, as well as several predefined specific data models to be used in reports. -""" \ No newline at end of file +""" diff --git a/cinder/openstack/common/report/models/base.py b/cinder/openstack/common/report/models/base.py index 90914ffe0..d840c5b51 100644 --- a/cinder/openstack/common/report/models/base.py +++ b/cinder/openstack/common/report/models/base.py @@ -24,6 +24,8 @@ the report serialization process. import collections as col import copy +import six + class ReportModel(col.MutableMapping): """A Report Data Model @@ -37,13 +39,29 @@ class ReportModel(col.MutableMapping): model. An appropriate object for a view is callable with a single parameter: the model to be serialized. - :param data: a dictionary of data to initially associate with the model + If present, the object passed in as data will be transformed + into a standard python dict. For mappings, this is fairly + straightforward. For sequences, the indices become keys + and the items become values. + + :param data: a sequence or mapping of data to associate with the model :param attached_view: a view object to attach to this model """ def __init__(self, data=None, attached_view=None): self.attached_view = attached_view - self.data = data or {} + + if data is not None: + if isinstance(data, col.Mapping): + self.data = dict(data) + elif isinstance(data, col.Sequence): + # convert a list [a, b, c] to a dict {0: a, 1: b, 2: c} + self.data = dict(enumerate(data)) + else: + raise TypeError('Data for the model must be a sequence ' + 'or mapping.') + else: + self.data = {} def __str__(self): self_cpy = copy.deepcopy(self) @@ -81,9 +99,16 @@ class ReportModel(col.MutableMapping): return self.data.__contains__(key) def __getattr__(self, attrname): + # Needed for deepcopy in Python3. That will avoid an infinite loop + # in __getattr__ . + if 'data' not in self.__dict__: + self.data = {} + try: return self.data[attrname] except KeyError: + # we don't have that key in data, and the + # model class doesn't have that attribute raise AttributeError( "'{cl}' object has no attribute '{an}'".format( cl=type(self).__name__, an=attrname @@ -96,19 +121,42 @@ class ReportModel(col.MutableMapping): def __iter__(self): return self.data.__iter__() - def set_current_view_type(self, tp): + def set_current_view_type(self, tp, visited=None): """Set the current view type This method attempts to set the current view type for this model and all submodels by calling - itself recursively on all values (and ignoring the - ones that are not themselves models) + itself recursively on all values, traversing + intervening sequences and mappings when possible, + and ignoring all other objects. :param tp: the type of the view ('text', 'json', 'xml', etc) + :param visited: a set of object ids for which the corresponding objects + have already had their view type set """ - for key in self: - try: - self[key].set_current_view_type(tp) - except AttributeError: - pass + if visited is None: + visited = set() + + def traverse_obj(obj): + oid = id(obj) + + # don't die on recursive structures, + # and don't treat strings like sequences + if oid in visited or isinstance(obj, six.string_types): + return + + visited.add(oid) + + if hasattr(obj, 'set_current_view_type'): + obj.set_current_view_type(tp, visited=visited) + + if isinstance(obj, col.Sequence): + for item in obj: + traverse_obj(item) + + elif isinstance(obj, col.Mapping): + for val in six.itervalues(obj): + traverse_obj(val) + + traverse_obj(self) diff --git a/cinder/openstack/common/report/models/conf.py b/cinder/openstack/common/report/models/conf.py index 77e24ab9f..f692d3c76 100644 --- a/cinder/openstack/common/report/models/conf.py +++ b/cinder/openstack/common/report/models/conf.py @@ -12,25 +12,25 @@ # License for the specific language governing permissions and limitations # under the License. -"""Provides Openstack Configuration Model +"""Provides OpenStack Configuration Model This module defines a class representing the data -model for :mod:`oslo.config` configuration options +model for :mod:`oslo_config` configuration options """ -import cinder.openstack.common.report.models.with_default_views as mwdv -import cinder.openstack.common.report.views.text.generic as generic_text_views +from cinder.openstack.common.report.models import with_default_views as mwdv +from cinder.openstack.common.report.views.text import generic as generic_text_views class ConfigModel(mwdv.ModelWithDefaultViews): """A Configuration Options Model This model holds data about a set of configuration options - from :mod:`oslo.config`. It supports both the default group + from :mod:`oslo_config`. It supports both the default group of options and named option groups. :param conf_obj: a configuration object - :type conf_obj: :class:`oslo.config.cfg.ConfigOpts` + :type conf_obj: :class:`oslo_config.cfg.ConfigOpts` """ def __init__(self, conf_obj): @@ -41,8 +41,15 @@ class ConfigModel(mwdv.ModelWithDefaultViews): def opt_title(optname, co): return co._opts[optname]['opt'].name + def opt_value(opt_obj, value): + if opt_obj['opt'].secret: + return '***' + else: + return value + self['default'] = dict( - (opt_title(optname, conf_obj), conf_obj[optname]) + (opt_title(optname, conf_obj), + opt_value(conf_obj._opts[optname], conf_obj[optname])) for optname in conf_obj._opts ) @@ -50,9 +57,10 @@ class ConfigModel(mwdv.ModelWithDefaultViews): for groupname in conf_obj._groups: group_obj = conf_obj._groups[groupname] curr_group_opts = dict( - (opt_title(optname, group_obj), conf_obj[groupname][optname]) - for optname in group_obj._opts - ) + (opt_title(optname, group_obj), + opt_value(group_obj._opts[optname], + conf_obj[groupname][optname])) + for optname in group_obj._opts) groups[group_obj.name] = curr_group_opts self.update(groups) diff --git a/cinder/openstack/common/report/models/process.py b/cinder/openstack/common/report/models/process.py new file mode 100644 index 000000000..22d1e98f7 --- /dev/null +++ b/cinder/openstack/common/report/models/process.py @@ -0,0 +1,62 @@ +# Copyright 2014 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Provides a process model + +This module defines a class representing a process, +potentially with subprocesses. +""" + +import cinder.openstack.common.report.models.with_default_views as mwdv +import cinder.openstack.common.report.views.text.process as text_views + + +class ProcessModel(mwdv.ModelWithDefaultViews): + """A Process Model + + This model holds data about a process, + including references to any subprocesses + + :param process: a :class:`psutil.Process` object + """ + + def __init__(self, process): + super(ProcessModel, self).__init__( + text_view=text_views.ProcessView()) + + self['pid'] = process.pid + self['parent_pid'] = process.ppid + if hasattr(process, 'uids'): + self['uids'] = {'real': process.uids.real, + 'effective': process.uids.effective, + 'saved': process.uids.saved} + else: + self['uids'] = {'real': None, + 'effective': None, + 'saved': None} + + if hasattr(process, 'gids'): + self['gids'] = {'real': process.gids.real, + 'effective': process.gids.effective, + 'saved': process.gids.saved} + else: + self['gids'] = {'real': None, + 'effective': None, + 'saved': None} + + self['username'] = process.username + self['command'] = process.cmdline + self['state'] = process.status + + self['children'] = [ProcessModel(pr) for pr in process.get_children()] diff --git a/cinder/openstack/common/report/models/threading.py b/cinder/openstack/common/report/models/threading.py index 15e1fdd02..94ccfcd98 100644 --- a/cinder/openstack/common/report/models/threading.py +++ b/cinder/openstack/common/report/models/threading.py @@ -20,8 +20,8 @@ thread, and stack trace data models import traceback -import cinder.openstack.common.report.models.with_default_views as mwdv -import cinder.openstack.common.report.views.text.threading as text_views +from cinder.openstack.common.report.models import with_default_views as mwdv +from cinder.openstack.common.report.views.text import threading as text_views class StackTraceModel(mwdv.ModelWithDefaultViews): @@ -42,12 +42,12 @@ class StackTraceModel(mwdv.ModelWithDefaultViews): {'filename': fn, 'line': ln, 'name': nm, 'code': cd} for fn, ln, nm, cd in traceback.extract_stack(stack_state) ] - - if stack_state.f_exc_type is not None: + # FIXME(flepied): under Python3 f_exc_type doesn't exist + # anymore so we lose information about exceptions + if getattr(stack_state, 'f_exc_type', None) is not None: self['root_exception'] = { 'type': stack_state.f_exc_type, - 'value': stack_state.f_exc_value - } + 'value': stack_state.f_exc_value} else: self['root_exception'] = None else: diff --git a/cinder/openstack/common/report/models/version.py b/cinder/openstack/common/report/models/version.py index 3666dc210..7602ce2a2 100644 --- a/cinder/openstack/common/report/models/version.py +++ b/cinder/openstack/common/report/models/version.py @@ -12,14 +12,14 @@ # License for the specific language governing permissions and limitations # under the License. -"""Provides Openstack Version Info Model +"""Provides OpenStack Version Info Model This module defines a class representing the data -model for Openstack package and version information +model for OpenStack package and version information """ -import cinder.openstack.common.report.models.with_default_views as mwdv -import cinder.openstack.common.report.views.text.generic as generic_text_views +from cinder.openstack.common.report.models import with_default_views as mwdv +from cinder.openstack.common.report.views.text import generic as generic_text_views class PackageModel(mwdv.ModelWithDefaultViews): diff --git a/cinder/openstack/common/report/models/with_default_views.py b/cinder/openstack/common/report/models/with_default_views.py index d1a8e42ba..43f6cbe8b 100644 --- a/cinder/openstack/common/report/models/with_default_views.py +++ b/cinder/openstack/common/report/models/with_default_views.py @@ -14,10 +14,10 @@ import copy -import cinder.openstack.common.report.models.base as base_model -import cinder.openstack.common.report.views.json.generic as jsonviews -import cinder.openstack.common.report.views.text.generic as textviews -import cinder.openstack.common.report.views.xml.generic as xmlviews +from cinder.openstack.common.report.models import base as base_model +from cinder.openstack.common.report.views.json import generic as jsonviews +from cinder.openstack.common.report.views.text import generic as textviews +from cinder.openstack.common.report.views.xml import generic as xmlviews class ModelWithDefaultViews(base_model.ReportModel): @@ -28,18 +28,18 @@ class ModelWithDefaultViews(base_model.ReportModel): when a submodel should have an attached view, but the view differs depending on the serialization format - Paramaters are as the superclass, with the exception - of any parameters ending in '_view': these parameters + Parameters are as the superclass, except for any + parameters ending in '_view': these parameters get stored as default views. The default 'default views' are text - :class:`openstack.common.views.text.generic.KeyValueView` + :class:`openstack.common.report.views.text.generic.KeyValueView` xml - :class:`openstack.common.views.xml.generic.KeyValueView` + :class:`openstack.common.report.views.xml.generic.KeyValueView` json - :class:`openstack.common.views.json.generic.KeyValueView` + :class:`openstack.common.report.views.json.generic.KeyValueView` .. function:: to_type() @@ -64,19 +64,18 @@ class ModelWithDefaultViews(base_model.ReportModel): del newargs[k] super(ModelWithDefaultViews, self).__init__(*args, **newargs) - def set_current_view_type(self, tp): + def set_current_view_type(self, tp, visited=None): self.attached_view = self.views[tp] - super(ModelWithDefaultViews, self).set_current_view_type(tp) + super(ModelWithDefaultViews, self).set_current_view_type(tp, visited) def __getattr__(self, attrname): if attrname[:3] == 'to_': if self.views[attrname[3:]] is not None: return lambda: self.views[attrname[3:]](self) else: - raise NotImplementedError(_( - "Model %(module)s.%(name)s does not have a default view " - "for %(tp)s"), {'module': type(self).__module__, - 'name': type(self).__name__, - 'tp': attrname[3:]}) + raise NotImplementedError(( + "Model {cn.__module__}.{cn.__name__} does not have" + + " a default view for " + "{tp}").format(cn=type(self), tp=attrname[3:])) else: return super(ModelWithDefaultViews, self).__getattr__(attrname) diff --git a/cinder/openstack/common/report/report.py b/cinder/openstack/common/report/report.py index 77f5d8533..96b070afd 100644 --- a/cinder/openstack/common/report/report.py +++ b/cinder/openstack/common/report/report.py @@ -14,12 +14,12 @@ """Provides Report classes -This module defines various classes representing -reports and report sections. All reports take the -form of a report class containing various report sections. +This module defines various classes representing reports and report sections. +All reports take the form of a report class containing various report +sections. """ -import cinder.openstack.common.report.views.text.header as header_views +from cinder.openstack.common.report.views.text import header as header_views class BasicReport(object): @@ -28,7 +28,7 @@ class BasicReport(object): A Basic Report consists of a collection of :class:`ReportSection` objects, each of which contains a top-level model and generator. It collects these sections into a cohesive report which may then - be serialized by calling :func:`run` + be serialized by calling :func:`run`. """ def __init__(self): @@ -78,10 +78,9 @@ class BasicReport(object): class ReportSection(object): """A Report Section - A report section contains a generator and a top-level view. - When something attempts to serialize the section by calling - str() on it, the section runs the generator and calls the view - on the resulting model. + A report section contains a generator and a top-level view. When something + attempts to serialize the section by calling str() on it, the section runs + the generator and calls the view on the resulting model. .. seealso:: @@ -90,8 +89,7 @@ class ReportSection(object): :param view: the top-level view for this section :param generator: the generator for this section - (any callable object which takes - no parameters and returns a data model) + (any callable object which takes no parameters and returns a data model) """ def __init__(self, view, generator): diff --git a/cinder/openstack/common/report/utils.py b/cinder/openstack/common/report/utils.py index 5f1dd1e7c..fb71e36ae 100644 --- a/cinder/openstack/common/report/utils.py +++ b/cinder/openstack/common/report/utils.py @@ -1,4 +1,4 @@ -# Copyright 2013 Red Hat, Inc. +# Copyright 2013 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain diff --git a/cinder/openstack/common/report/views/__init__.py b/cinder/openstack/common/report/views/__init__.py index 834f101ce..612959b2e 100644 --- a/cinder/openstack/common/report/views/__init__.py +++ b/cinder/openstack/common/report/views/__init__.py @@ -19,4 +19,4 @@ for use in reports. It is separated by type (xml, json, or text). Each type contains a submodule called 'generic' containing several basic, universal views for that type. There is also a predefined view that utilizes Jinja. -""" \ No newline at end of file +""" diff --git a/cinder/openstack/common/report/views/jinja_view.py b/cinder/openstack/common/report/views/jinja_view.py index a6f340e8e..5f57dc34a 100644 --- a/cinder/openstack/common/report/views/jinja_view.py +++ b/cinder/openstack/common/report/views/jinja_view.py @@ -19,6 +19,8 @@ system for serialization. For more information on Jinja, please see http://jinja.pocoo.org/ . """ +import copy + import jinja2 @@ -79,6 +81,16 @@ class JinjaView(object): def __call__(self, model): return self.template.render(**model) + def __deepcopy__(self, memodict): + res = object.__new__(JinjaView) + res._text = copy.deepcopy(self._text, memodict) + + # regenerate the template on a deepcopy + res._regentemplate = True + res._templatecache = None + + return res + @property def template(self): """Get the Compiled Template diff --git a/cinder/openstack/common/report/views/json/__init__.py b/cinder/openstack/common/report/views/json/__init__.py index 2b3933e0c..47bd33b6f 100644 --- a/cinder/openstack/common/report/views/json/__init__.py +++ b/cinder/openstack/common/report/views/json/__init__.py @@ -16,4 +16,4 @@ This module provides several basic views which serialize models into JSON. -""" \ No newline at end of file +""" diff --git a/cinder/openstack/common/report/views/json/generic.py b/cinder/openstack/common/report/views/json/generic.py index 079c01872..f2769e31c 100644 --- a/cinder/openstack/common/report/views/json/generic.py +++ b/cinder/openstack/common/report/views/json/generic.py @@ -27,7 +27,7 @@ import copy from oslo_serialization import jsonutils as json -import cinder.openstack.common.report.utils as utils +from cinder.openstack.common.report import utils as utils class BasicKeyValueView(object): @@ -57,10 +57,10 @@ class KeyValueView(object): def __call__(self, model): # this part deals with subviews that were already serialized cpy = copy.deepcopy(model) - for key, valstr in model.items(): - if getattr(valstr, '__is_json__', False): - cpy[key] = json.loads(valstr) + for key in model.keys(): + if getattr(model[key], '__is_json__', False): + cpy[key] = json.loads(model[key]) - res = utils.StringWithAttrs(json.dumps(cpy.data)) + res = utils.StringWithAttrs(json.dumps(cpy.data, sort_keys=True)) res.__is_json__ = True return res diff --git a/cinder/openstack/common/report/views/text/__init__.py b/cinder/openstack/common/report/views/text/__init__.py index 3f17fdeeb..c09748443 100644 --- a/cinder/openstack/common/report/views/text/__init__.py +++ b/cinder/openstack/common/report/views/text/__init__.py @@ -16,4 +16,4 @@ This module provides several basic views which serialize models into human-readable text. -""" \ No newline at end of file +""" diff --git a/cinder/openstack/common/report/views/text/generic.py b/cinder/openstack/common/report/views/text/generic.py index 736383305..3b30a0707 100644 --- a/cinder/openstack/common/report/views/text/generic.py +++ b/cinder/openstack/common/report/views/text/generic.py @@ -120,7 +120,7 @@ class KeyValueView(object): if self.before_dict is not None: res.insert(0, self.before_dict) - for key in root: + for key in sorted(root): res.extend(serialize(root[key], key, indent + 1)) elif (isinstance(root, col.Sequence) and not isinstance(root, six.string_types)): @@ -129,7 +129,7 @@ class KeyValueView(object): if self.before_list is not None: res.insert(0, self.before_list) - for val in root: + for val in sorted(root, key=str): res.extend(serialize(val, None, indent + 1)) else: str_root = str(root) @@ -172,7 +172,7 @@ class TableView(object): self.table_prop_name = table_prop_name self.column_names = column_names self.column_values = column_values - self.column_width = (72 - len(column_names) + 1) / len(column_names) + self.column_width = (72 - len(column_names) + 1) // len(column_names) column_headers = "|".join( "{ch[" + str(n) + "]: ^" + str(self.column_width) + "}" diff --git a/cinder/openstack/common/report/views/text/header.py b/cinder/openstack/common/report/views/text/header.py index aa4c72a61..58d06c0d3 100644 --- a/cinder/openstack/common/report/views/text/header.py +++ b/cinder/openstack/common/report/views/text/header.py @@ -49,4 +49,3 @@ class TitledView(HeaderView): def __init__(self, title): super(TitledView, self).__init__(self.FORMAT_STR.format(title)) - diff --git a/cinder/openstack/common/report/views/text/process.py b/cinder/openstack/common/report/views/text/process.py new file mode 100644 index 000000000..c7d7f12d6 --- /dev/null +++ b/cinder/openstack/common/report/views/text/process.py @@ -0,0 +1,38 @@ +# Copyright 2014 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Provides process view + +This module provides a view for +visualizing processes in human-readable formm +""" + +import cinder.openstack.common.report.views.jinja_view as jv + + +class ProcessView(jv.JinjaView): + """A Process View + + This view displays process models defined by + :class:`openstack.common.report.models.process.ProcessModel` + """ + + VIEW_TEXT = ( + "Process {{ pid }} (under {{ parent_pid }}) " + "[ run by: {{ username }} ({{ uids.real|default('unknown uid') }})," + " state: {{ state }} ]\n" + "{% for child in children %}" + " {{ child }}" + "{% endfor %}" + ) diff --git a/cinder/openstack/common/report/views/text/threading.py b/cinder/openstack/common/report/views/text/threading.py index df63b84f3..c4a11c079 100644 --- a/cinder/openstack/common/report/views/text/threading.py +++ b/cinder/openstack/common/report/views/text/threading.py @@ -19,7 +19,7 @@ visualizing threads, green threads, and stack traces in human-readable form. """ -import cinder.openstack.common.report.views.jinja_view as jv +from cinder.openstack.common.report.views import jinja_view as jv class StackTraceView(jv.JinjaView): @@ -52,7 +52,7 @@ class GreenThreadView(object): """A Green Thread View This view displays a green thread provided by the data - model :class:`openstack.common.report.models.threading.GreenThreadModel` # noqa + model :class:`openstack.common.report.models.threading.GreenThreadModel` """ FORMAT_STR = "------{thread_str: ^60}------" + "\n" + "{stack_trace}" diff --git a/cinder/openstack/common/report/views/xml/__init__.py b/cinder/openstack/common/report/views/xml/__init__.py index bfcc8511d..a40fec984 100644 --- a/cinder/openstack/common/report/views/xml/__init__.py +++ b/cinder/openstack/common/report/views/xml/__init__.py @@ -16,4 +16,4 @@ This module provides several basic views which serialize models into XML. -""" \ No newline at end of file +""" diff --git a/cinder/openstack/common/report/views/xml/generic.py b/cinder/openstack/common/report/views/xml/generic.py index 96337fafd..59c3f4861 100644 --- a/cinder/openstack/common/report/views/xml/generic.py +++ b/cinder/openstack/common/report/views/xml/generic.py @@ -29,7 +29,7 @@ import xml.etree.ElementTree as ET import six -import cinder.openstack.common.report.utils as utils +from cinder.openstack.common.report import utils as utils class KeyValueView(object): @@ -66,11 +66,11 @@ class KeyValueView(object): res = ET.Element(rootkeyname) if isinstance(rootmodel, col.Mapping): - for key in rootmodel: + for key in sorted(rootmodel): res.append(serialize(rootmodel[key], key)) elif (isinstance(rootmodel, col.Sequence) and not isinstance(rootmodel, six.string_types)): - for val in rootmodel: + for val in sorted(rootmodel, key=str): res.append(serialize(val, 'item')) elif ET.iselement(rootmodel): res.append(rootmodel) @@ -79,7 +79,9 @@ class KeyValueView(object): return res - res = utils.StringWithAttrs(ET.tostring(serialize(cpy, - self.wrapper_name))) + str_ = ET.tostring(serialize(cpy, + self.wrapper_name), + encoding="utf-8").decode("utf-8") + res = utils.StringWithAttrs(str_) res.__is_xml__ = True return res diff --git a/openstack-common.conf b/openstack-common.conf index 7c134b773..467a13d16 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -15,6 +15,12 @@ module=scheduler.weights module=service module=versionutils module=report +module=report.generators +module=report.models +module=report.views +module=report.views.json +module=report.views.text +module=report.views.xml # The base module to hold the copy of openstack.common base=cinder diff --git a/requirements.txt b/requirements.txt index d78ca2256..0e3b5c0b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,6 +28,7 @@ osprofiler>=0.3.0 # Apache-2.0 paramiko>=1.13.0 Paste PasteDeploy>=1.5.0 +psutil>=1.1.1,<2.0.0 pycrypto>=2.6 pyparsing>=2.0.1 python-barbicanclient>=3.0.1 -- 2.45.2