Introduce Guru Meditation Reports into Designate

* Pick GMR implementation codes from oslo-incubator
* Add SIGUSR1 signal handlers in each Designate service
* Add GMR docs

Change-Id: I76c82f5e128ddcd294be43407ef5c254747a874e
Implements: blueprint guru-meditation-reports
This commit is contained in:
stanzgy 2015-03-10 17:14:21 +08:00
parent 6bf5ae1938
commit 585378840a
40 changed files with 2504 additions and 0 deletions

View File

@ -30,6 +30,7 @@ CONF.import_opt('workers', 'designate.agent', group='service:agent')
def main(): def main():
utils.read_config('designate', sys.argv) utils.read_config('designate', sys.argv)
logging.setup(CONF, 'designate') logging.setup(CONF, 'designate')
utils.setup_gmr(log_dir=cfg.CONF.log_dir)
server = agent_service.Service() server = agent_service.Service()
service.serve(server, workers=CONF['service:agent'].workers) service.serve(server, workers=CONF['service:agent'].workers)

View File

@ -31,6 +31,7 @@ CONF.import_opt('workers', 'designate.api', group='service:api')
def main(): def main():
utils.read_config('designate', sys.argv) utils.read_config('designate', sys.argv)
logging.setup(CONF, 'designate') logging.setup(CONF, 'designate')
utils.setup_gmr(log_dir=cfg.CONF.log_dir)
rpc.init(CONF) rpc.init(CONF)

View File

@ -30,6 +30,7 @@ CONF.import_opt('workers', 'designate.central', group='service:central')
def main(): def main():
utils.read_config('designate', sys.argv) utils.read_config('designate', sys.argv)
logging.setup(CONF, 'designate') logging.setup(CONF, 'designate')
utils.setup_gmr(log_dir=cfg.CONF.log_dir)
server = central.Service() server = central.Service()
service.serve(server, workers=CONF['service:central'].workers) service.serve(server, workers=CONF['service:central'].workers)

View File

@ -119,6 +119,7 @@ def main():
print(_('Please re-run designate-manage as root.')) print(_('Please re-run designate-manage as root.'))
sys.exit(2) sys.exit(2)
utils.setup_gmr(log_dir=cfg.CONF.log_dir)
fn = CONF.category.action_fn fn = CONF.category.action_fn
fn_args = fetch_func_args(fn) fn_args = fetch_func_args(fn)

View File

@ -30,6 +30,7 @@ CONF.import_opt('workers', 'designate.mdns', group='service:mdns')
def main(): def main():
utils.read_config('designate', sys.argv) utils.read_config('designate', sys.argv)
logging.setup(CONF, 'designate') logging.setup(CONF, 'designate')
utils.setup_gmr(log_dir=cfg.CONF.log_dir)
server = mdns_service.Service() server = mdns_service.Service()
service.serve(server, workers=CONF['service:mdns'].workers) service.serve(server, workers=CONF['service:mdns'].workers)

View File

@ -31,6 +31,7 @@ CONF.import_opt('workers', 'designate.pool_manager',
def main(): def main():
utils.read_config('designate', sys.argv) utils.read_config('designate', sys.argv)
logging.setup(CONF, 'designate') logging.setup(CONF, 'designate')
utils.setup_gmr(log_dir=cfg.CONF.log_dir)
server = pool_manager_service.Service() server = pool_manager_service.Service()
service.serve(server, workers=CONF['service:pool_manager'].workers) service.serve(server, workers=CONF['service:pool_manager'].workers)

View File

@ -30,6 +30,7 @@ CONF.import_opt('workers', 'designate.sink', group='service:sink')
def main(): def main():
utils.read_config('designate', sys.argv) utils.read_config('designate', sys.argv)
logging.setup(CONF, 'designate') logging.setup(CONF, 'designate')
utils.setup_gmr(log_dir=cfg.CONF.log_dir)
server = sink_service.Service() server = sink_service.Service()
service.serve(server, workers=CONF['service:sink'].workers) service.serve(server, workers=CONF['service:sink'].workers)

View File

@ -0,0 +1,25 @@
# 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
# 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 way to generate serializable reports
This package/module provides mechanisms for defining reports
which may then be serialized into various data types. Each
report ( :class:`openstack.common.report.report.BasicReport` )
is composed of one or more report sections
( :class:`openstack.common.report.report.BasicSection` ),
which contain generators which generate data models
( :class:`openstack.common.report.models.base.ReportModels` ),
which are then serialized by views.
"""

View File

@ -0,0 +1,21 @@
# 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
# 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 Data Model Generators
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.
"""

View File

@ -0,0 +1,44 @@
# 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
# 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 OpenStack config generators
This module defines a class for configuration
generators for generating the model in
:mod:`openstack.common.report.models.conf`.
"""
from oslo_config import cfg
from designate.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`,
by default using the configuration options stored
in :attr:`oslo_config.cfg.CONF`, which is where
OpenStack stores everything.
:param cnf: the configuration option object
:type cnf: :class:`oslo_config.cfg.ConfigOpts`
"""
def __init__(self, cnf=cfg.CONF):
self.conf_obj = cnf
def __call__(self):
return cm.ConfigModel(self.conf_obj)

View File

@ -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 designate.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()))

View File

@ -0,0 +1,86 @@
# 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
# 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 thread-related generators
This module defines classes for threading-related
generators for generating the models in
:mod:`openstack.common.report.models.threading`.
"""
from __future__ import absolute_import
import sys
import threading
import greenlet
from designate.openstack.common.report.models import threading as tm
from designate.openstack.common.report.models import with_default_views as mwdv
from designate.openstack.common.report import utils as rutils
from designate.openstack.common.report.views.text import generic as text_views
class ThreadReportGenerator(object):
"""A Thread Data Generator
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()` . 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 = 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)
return mwdv.ModelWithDefaultViews(threadModels,
text_view=text_views.MultiView())
class GreenThreadReportGenerator(object):
"""A Green Thread Data Generator
This generator returns a collection of
:class:`openstack.common.report.models.threading.GreenThreadModel`
objects by introspecting the current python garbage collection
state, and sifting through for :class:`greenlet.greenlet` objects.
.. seealso::
Function :func:`openstack.common.report.utils._find_objects`
"""
def __call__(self):
threadModels = [
tm.GreenThreadModel(gr.gr_frame)
for gr in rutils._find_objects(greenlet.greenlet)
]
return mwdv.ModelWithDefaultViews(threadModels,
text_view=text_views.MultiView())

View File

@ -0,0 +1,46 @@
# 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
# 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 OpenStack version generators
This module defines a class for OpenStack
version and package information
generators for generating the model in
:mod:`openstack.common.report.models.version`.
"""
from designate.openstack.common.report.models import version as vm
class PackageReportGenerator(object):
"""A Package Information Data Generator
This generator returns
:class:`openstack.common.report.models.version.PackageModel`,
extracting data from the given version object, which should follow
the general format defined in Nova's version information (i.e. it
should contain the methods vendor_string, product_string, and
version_string_with_package).
:param version_object: the version information object
"""
def __init__(self, version_obj):
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())

View File

@ -0,0 +1,226 @@
# 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
# 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 Guru Meditation Report
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):
.. code-block:: python
:emphasize-lines: 8,9,10
CONF = cfg.CONF
# maybe import some options here...
def main():
config.parse_args(sys.argv)
logging.setup('blah')
TextGuruMeditation.register_section('Some Special Section',
special_section_generator)
TextGuruMeditation.setup_autorun(version_object)
server = service.Service.create(binary='some-service',
topic=CONF.some_service_topic)
service.serve(server)
service.wait()
Then, you can do
.. code-block:: bash
$ kill -USR1 $SERVICE_PID
and get a Guru Meditation Report in the file or terminal
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 designate.openstack.common.report.generators import conf as cgen
from designate.openstack.common.report.generators import process as prgen
from designate.openstack.common.report.generators import threading as tgen
from designate.openstack.common.report.generators import version as pgen
from designate.openstack.common.report import report
class GuruMeditation(object):
"""A Guru Meditation Report Mixin/Base Class
This class is a base class for Guru Meditation Reports.
It provides facilities for registering sections and
setting up functionality to auto-run the report on
a certain signal.
This class should always be used in conjunction with
a Report class via multiple inheritance. It should
always come first in the class list to ensure the
MRO is correct.
"""
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)
@classmethod
def register_section(cls, section_title, generator):
"""Register a New Section
This method registers a persistent section for the current
class.
:param str section_title: the title of the section
:param generator: the generator for the section
"""
try:
cls.persistent_sections.append([section_title, generator])
except AttributeError:
cls.persistent_sections = [[section_title, generator]]
@classmethod
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 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
"""
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, service_name, log_dir, traceback):
"""The Signal Handler
This method (indirectly) handles receiving a registered signal and
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, traceback).run()
except Exception:
print("Unable to run Guru Meditation Report!",
file=sys.stderr)
else:
if log_dir:
service_name = service_name or os.path.basename(
inspect.stack()[-1][1])
filename = "%s_gurumeditation_%s" % (
service_name, timeutils.strtime(fmt=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:]
self.add_section('Package',
pgen.PackageReportGenerator(self.version_obj))
self.add_section('Threads',
tgen.ThreadReportGenerator(self.traceback))
self.add_section('Green Threads',
tgen.GreenThreadReportGenerator())
self.add_section('Processes',
prgen.ProcessReportGenerator())
self.add_section('Configuration',
cgen.ConfigReportGenerator())
try:
for section_title, generator in self.persistent_sections:
self.add_section(section_title, generator)
except AttributeError:
pass
def run(self):
self._readd_sections()
return super(GuruMeditation, self).run()
# GuruMeditation must come first to get the correct MRO
class TextGuruMeditation(GuruMeditation, report.TextReport):
"""A Text Guru Meditation Report
This report is the basic human-readable Guru Meditation Report
It contains the following sections by default
(in addition to any registered persistent sections):
- Package Information
- Threads List
- 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, traceback=None):
super(TextGuruMeditation, self).__init__(version_obj, traceback,
'Guru Meditation')

View File

@ -0,0 +1,20 @@
# 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
# 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 data models
This module provides both the base data model,
as well as several predefined specific data models
to be used in reports.
"""

View File

@ -0,0 +1,162 @@
# 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
# 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 the base report model
This module defines a class representing the basic report
data model from which all data models should inherit (or
at least implement similar functionality). Data models
store unserialized data generated by generators during
the report serialization process.
"""
import collections as col
import copy
import six
class ReportModel(col.MutableMapping):
"""A Report Data Model
A report data model contains data generated by some
generator method or class. Data may be read or written
using dictionary-style access, and may be read (but not
written) using object-member-style access. Additionally,
a data model may have an associated view. This view is
used to serialize the model when str() is called on the
model. An appropriate object for a view is callable with
a single parameter: the model to be serialized.
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
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)
for key in self_cpy:
if getattr(self_cpy[key], 'attached_view', None) is not None:
self_cpy[key] = str(self_cpy[key])
if self.attached_view is not None:
return self.attached_view(self_cpy)
else:
raise Exception("Cannot stringify model: no attached view")
def __repr__(self):
if self.attached_view is not None:
return ("<Model {cl.__module__}.{cl.__name__} {dt}"
" with view {vw.__module__}."
"{vw.__name__}>").format(cl=type(self),
dt=self.data,
vw=type(self.attached_view))
else:
return ("<Model {cl.__module__}.{cl.__name__} {dt}"
" with no view>").format(cl=type(self),
dt=self.data)
def __getitem__(self, attrname):
return self.data[attrname]
def __setitem__(self, attrname, attrval):
self.data[attrname] = attrval
def __delitem__(self, attrname):
del self.data[attrname]
def __contains__(self, key):
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
)
)
def __len__(self):
return len(self.data)
def __iter__(self):
return self.data.__iter__()
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, 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
"""
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)

View File

@ -0,0 +1,66 @@
# 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
# 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 OpenStack Configuration Model
This module defines a class representing the data
model for :mod:`oslo_config` configuration options
"""
from designate.openstack.common.report.models import with_default_views as mwdv
from designate.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
of options and named option groups.
:param conf_obj: a configuration object
:type conf_obj: :class:`oslo_config.cfg.ConfigOpts`
"""
def __init__(self, conf_obj):
kv_view = generic_text_views.KeyValueView(dict_sep=": ",
before_dict='')
super(ConfigModel, self).__init__(text_view=kv_view)
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),
opt_value(conf_obj._opts[optname], conf_obj[optname]))
for optname in conf_obj._opts
)
groups = {}
for groupname in conf_obj._groups:
group_obj = conf_obj._groups[groupname]
curr_group_opts = dict(
(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)

View File

@ -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 designate.openstack.common.report.models.with_default_views as mwdv
import designate.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()]

View File

@ -0,0 +1,100 @@
# 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
# 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 threading and stack-trace models
This module defines classes representing thread, green
thread, and stack trace data models
"""
import traceback
from designate.openstack.common.report.models import with_default_views as mwdv
from designate.openstack.common.report.views.text import threading as text_views
class StackTraceModel(mwdv.ModelWithDefaultViews):
"""A Stack Trace Model
This model holds data from a python stack trace,
commonly extracted from running thread information
:param stack_state: the python stack_state object
"""
def __init__(self, stack_state):
super(StackTraceModel, self).__init__(
text_view=text_views.StackTraceView())
if (stack_state is not None):
self['lines'] = [
{'filename': fn, 'line': ln, 'name': nm, 'code': cd}
for fn, ln, nm, cd in traceback.extract_stack(stack_state)
]
# 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}
else:
self['root_exception'] = None
else:
self['lines'] = []
self['root_exception'] = None
class ThreadModel(mwdv.ModelWithDefaultViews):
"""A Thread Model
This model holds data for information about an
individual thread. It holds both a thread id,
as well as a stack trace for the thread
.. seealso::
Class :class:`StackTraceModel`
:param int thread_id: the id of the thread
:param stack: the python stack state for the current thread
"""
# threadId, stack in sys._current_frams().items()
def __init__(self, thread_id, stack):
super(ThreadModel, self).__init__(text_view=text_views.ThreadView())
self['thread_id'] = thread_id
self['stack_trace'] = StackTraceModel(stack)
class GreenThreadModel(mwdv.ModelWithDefaultViews):
"""A Green Thread Model
This model holds data for information about an
individual thread. Unlike the thread model,
it holds just a stack trace, since green threads
do not have thread ids.
.. seealso::
Class :class:`StackTraceModel`
:param stack: the python stack state for the green thread
"""
# gr in greenpool.coroutines_running --> gr.gr_frame
def __init__(self, stack):
super(GreenThreadModel, self).__init__(
{'stack_trace': StackTraceModel(stack)},
text_view=text_views.GreenThreadView())

View File

@ -0,0 +1,44 @@
# 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
# 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 OpenStack Version Info Model
This module defines a class representing the data
model for OpenStack package and version information
"""
from designate.openstack.common.report.models import with_default_views as mwdv
from designate.openstack.common.report.views.text import generic as generic_text_views
class PackageModel(mwdv.ModelWithDefaultViews):
"""A Package Information Model
This model holds information about the current
package. It contains vendor, product, and version
information.
:param str vendor: the product vendor
:param str product: the product name
:param str version: the product version
"""
def __init__(self, vendor, product, version):
super(PackageModel, self).__init__(
text_view=generic_text_views.KeyValueView()
)
self['vendor'] = vendor
self['product'] = product
self['version'] = version

View File

@ -0,0 +1,81 @@
# 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
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
from designate.openstack.common.report.models import base as base_model
from designate.openstack.common.report.views.json import generic as jsonviews
from designate.openstack.common.report.views.text import generic as textviews
from designate.openstack.common.report.views.xml import generic as xmlviews
class ModelWithDefaultViews(base_model.ReportModel):
"""A Model With Default Views of Various Types
A model with default views has several predefined views,
each associated with a given type. This is often used for
when a submodel should have an attached view, but the view
differs depending on the serialization format
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.report.views.text.generic.KeyValueView`
xml
:class:`openstack.common.report.views.xml.generic.KeyValueView`
json
:class:`openstack.common.report.views.json.generic.KeyValueView`
.. function:: to_type()
('type' is one of the 'default views' defined for this model)
Serializes this model using the default view for 'type'
:rtype: str
:returns: this model serialized as 'type'
"""
def __init__(self, *args, **kwargs):
self.views = {
'text': textviews.KeyValueView(),
'json': jsonviews.KeyValueView(),
'xml': xmlviews.KeyValueView()
}
newargs = copy.copy(kwargs)
for k in kwargs:
if k.endswith('_view'):
self.views[k[:-5]] = kwargs[k]
del newargs[k]
super(ModelWithDefaultViews, self).__init__(*args, **newargs)
def set_current_view_type(self, tp, visited=None):
self.attached_view = self.views[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 {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)

View File

@ -0,0 +1,187 @@
# 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
# 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 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.
"""
from designate.openstack.common.report.views.text import header as header_views
class BasicReport(object):
"""A Basic Report
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`.
"""
def __init__(self):
self.sections = []
self._state = 0
def add_section(self, view, generator, index=None):
"""Add a section to the report
This method adds a section with the given view and
generator to the report. An index may be specified to
insert the section at a given location in the list;
If no index is specified, the section is appended to the
list. The view is called on the model which results from
the generator when the report is run. A generator is simply
a method or callable object which takes no arguments and
returns a :class:`openstack.common.report.models.base.ReportModel`
or similar object.
:param view: the top-level view for the section
:param generator: the method or class which generates the model
:param index: the index at which to insert the section
(or None to append it)
:type index: int or None
"""
if index is None:
self.sections.append(ReportSection(view, generator))
else:
self.sections.insert(index, ReportSection(view, generator))
def run(self):
"""Run the report
This method runs the report, having each section generate
its data and serialize itself before joining the sections
together. The BasicReport accomplishes the joining
by joining the serialized sections together with newlines.
:rtype: str
:returns: the serialized report
"""
return "\n".join(str(sect) for sect in self.sections)
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.
.. seealso::
Class :class:`BasicReport`
:func:`BasicReport.add_section`
: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)
"""
def __init__(self, view, generator):
self.view = view
self.generator = generator
def __str__(self):
return self.view(self.generator())
class ReportOfType(BasicReport):
"""A Report of a Certain Type
A ReportOfType has a predefined type associated with it.
This type is automatically propagated down to the each of
the sections upon serialization by wrapping the generator
for each section.
.. seealso::
Class :class:`openstack.common.report.models.with_default_view.ModelWithDefaultView` # noqa
(the entire class)
Class :class:`openstack.common.report.models.base.ReportModel`
:func:`openstack.common.report.models.base.ReportModel.set_current_view_type` # noqa
:param str tp: the type of the report
"""
def __init__(self, tp):
self.output_type = tp
super(ReportOfType, self).__init__()
def add_section(self, view, generator, index=None):
def with_type(gen):
def newgen():
res = gen()
try:
res.set_current_view_type(self.output_type)
except AttributeError:
pass
return res
return newgen
super(ReportOfType, self).add_section(
view,
with_type(generator),
index
)
class TextReport(ReportOfType):
"""A Human-Readable Text Report
This class defines a report that is designed to be read by a human
being. It has nice section headers, and a formatted title.
:param str name: the title of the report
"""
def __init__(self, name):
super(TextReport, self).__init__('text')
self.name = name
# add a title with a generator that creates an empty result model
self.add_section(name, lambda: ('|' * 72) + "\n\n")
def add_section(self, heading, generator, index=None):
"""Add a section to the report
This method adds a section with the given title, and
generator to the report. An index may be specified to
insert the section at a given location in the list;
If no index is specified, the section is appended to the
list. The view is called on the model which results from
the generator when the report is run. A generator is simply
a method or callable object which takes no arguments and
returns a :class:`openstack.common.report.models.base.ReportModel`
or similar object.
The model is told to serialize as text (if possible) at serialization
time by wrapping the generator. The view model's attached view
(if any) is wrapped in a
:class:`openstack.common.report.views.text.header.TitledView`
:param str heading: the title for the section
:param generator: the method or class which generates the model
:param index: the index at which to insert the section
(or None to append)
:type index: int or None
"""
super(TextReport, self).add_section(header_views.TitledView(heading),
generator,
index)

View File

@ -0,0 +1,46 @@
# 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
# 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.
"""Various utilities for report generation
This module includes various utilities
used in generating reports.
"""
import gc
class StringWithAttrs(str):
"""A String that can have arbitrary attributes
"""
pass
def _find_objects(t):
"""Find Objects in the GC State
This horribly hackish method locates objects of a
given class in the current python instance's garbage
collection state. In case you couldn't tell, this is
horribly hackish, but is necessary for locating all
green threads, since they don't keep track of themselves
like normal threads do in python.
:param class t: the class of object to locate
:rtype: list
:returns: a list of objects of the given type
"""
return [o for o in gc.get_objects() if isinstance(o, t)]

View File

@ -0,0 +1,22 @@
# 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
# 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 predefined views
This module provides a collection of predefined views
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.
"""

View File

@ -0,0 +1,137 @@
# 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
# 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 Jinja Views
This module provides views that utilize the Jinja templating
system for serialization. For more information on Jinja, please
see http://jinja.pocoo.org/ .
"""
import copy
import jinja2
class JinjaView(object):
"""A Jinja View
This view renders the given model using the provided Jinja
template. The template can be given in various ways.
If the `VIEw_TEXT` property is defined, that is used as template.
Othewise, if a `path` parameter is passed to the constructor, that
is used to load a file containing the template. If the `path`
parameter is None, the `text` parameter is used as the template.
The leading newline character and trailing newline character are stripped
from the template (provided they exist). Baseline indentation is
also stripped from each line. The baseline indentation is determined by
checking the indentation of the first line, after stripping off the leading
newline (if any).
:param str path: the path to the Jinja template
:param str text: the text of the Jinja template
"""
def __init__(self, path=None, text=None):
try:
self._text = self.VIEW_TEXT
except AttributeError:
if path is not None:
with open(path, 'r') as f:
self._text = f.read()
elif text is not None:
self._text = text
else:
self._text = ""
if self._text[0] == "\n":
self._text = self._text[1:]
newtext = self._text.lstrip()
amt = len(self._text) - len(newtext)
if (amt > 0):
base_indent = self._text[0:amt]
lines = self._text.splitlines()
newlines = []
for line in lines:
if line.startswith(base_indent):
newlines.append(line[amt:])
else:
newlines.append(line)
self._text = "\n".join(newlines)
if self._text[-1] == "\n":
self._text = self._text[:-1]
self._regentemplate = True
self._templatecache = None
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
Gets the compiled template, using a cached copy if possible
(stored in attr:`_templatecache`) or otherwise recompiling
the template if the compiled template is not present or is
invalid (due to attr:`_regentemplate` being set to True).
:returns: the compiled Jinja template
:rtype: :class:`jinja2.Template`
"""
if self._templatecache is None or self._regentemplate:
self._templatecache = jinja2.Template(self._text)
self._regentemplate = False
return self._templatecache
def _gettext(self):
"""Get the Template Text
Gets the text of the current template
:returns: the text of the Jinja template
:rtype: str
"""
return self._text
def _settext(self, textval):
"""Set the Template Text
Sets the text of the current template, marking it
for recompilation next time the compiled template
is retrived via attr:`template` .
:param str textval: the new text of the Jinja template
"""
self._text = textval
self.regentemplate = True
text = property(_gettext, _settext)

View File

@ -0,0 +1,19 @@
# 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
# 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 basic JSON views
This module provides several basic views which serialize
models into JSON.
"""

View File

@ -0,0 +1,66 @@
# 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
# 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 generic JSON views
This modules defines several basic views for serializing
data to JSON. Submodels that have already been serialized
as JSON may have their string values marked with `__is_json__
= True` using :class:`openstack.common.report.utils.StringWithAttrs`
(each of the classes within this module does this automatically,
and non-naive serializers check for this attribute and handle
such strings specially)
"""
import copy
from oslo_serialization import jsonutils as json
from designate.openstack.common.report import utils as utils
class BasicKeyValueView(object):
"""A Basic Key-Value JSON View
This view performs a naive serialization of a model
into JSON by simply calling :func:`json.dumps` on the model
"""
def __call__(self, model):
res = utils.StringWithAttrs(json.dumps(model.data))
res.__is_json__ = True
return res
class KeyValueView(object):
"""A Key-Value JSON View
This view performs advanced serialization to a model
into JSON. It does so by first checking all values to
see if they are marked as JSON. If so, they are deserialized
using :func:`json.loads`. Then, the copy of the model with all
JSON deserialized is reserialized into proper nested JSON using
:func:`json.dumps`.
"""
def __call__(self, model):
# this part deals with subviews that were already serialized
cpy = copy.deepcopy(model)
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, sort_keys=True))
res.__is_json__ = True
return res

View File

@ -0,0 +1,19 @@
# 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
# 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 basic text views
This module provides several basic views which serialize
models into human-readable text.
"""

View File

@ -0,0 +1,202 @@
# 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
# 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 generic text views
This modules provides several generic views for
serializing models into human-readable text.
"""
import collections as col
import six
class MultiView(object):
"""A Text View Containing Multiple Views
This view simply serializes each
value in the data model, and then
joins them with newlines (ignoring
the key values altogether). This is
useful for serializing lists of models
(as array-like dicts).
"""
def __call__(self, model):
res = [str(model[key]) for key in model]
return "\n".join(res)
class BasicKeyValueView(object):
"""A Basic Key-Value Text View
This view performs a naive serialization of a model into
text using a basic key-value method, where each
key-value pair is rendered as "key = str(value)"
"""
def __call__(self, model):
res = ""
for key in model:
res += "{key} = {value}\n".format(key=key, value=model[key])
return res
class KeyValueView(object):
"""A Key-Value Text View
This view performs an advanced serialization of a model
into text by following the following set of rules:
key : text
key = text
rootkey : Mapping
::
rootkey =
serialize(key, value)
key : Sequence
::
key =
serialize(item)
:param str indent_str: the string used to represent one "indent"
:param str key_sep: the separator to use between keys and values
:param str dict_sep: the separator to use after a dictionary root key
:param str list_sep: the separator to use after a list root key
:param str anon_dict: the "key" to use when there is a dict in a list
(does not automatically use the dict separator)
:param before_dict: content to place on the line(s) before the a dict
root key (use None to avoid inserting an extra line)
:type before_dict: str or None
:param before_list: content to place on the line(s) before the a list
root key (use None to avoid inserting an extra line)
:type before_list: str or None
"""
def __init__(self,
indent_str=' ',
key_sep=' = ',
dict_sep=' = ',
list_sep=' = ',
anon_dict='[dict]',
before_dict=None,
before_list=None):
self.indent_str = indent_str
self.key_sep = key_sep
self.dict_sep = dict_sep
self.list_sep = list_sep
self.anon_dict = anon_dict
self.before_dict = before_dict
self.before_list = before_list
def __call__(self, model):
def serialize(root, rootkey, indent):
res = []
if rootkey is not None:
res.append((self.indent_str * indent) + rootkey)
if isinstance(root, col.Mapping):
if rootkey is None and indent > 0:
res.append((self.indent_str * indent) + self.anon_dict)
elif rootkey is not None:
res[0] += self.dict_sep
if self.before_dict is not None:
res.insert(0, self.before_dict)
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)):
if rootkey is not None:
res[0] += self.list_sep
if self.before_list is not None:
res.insert(0, self.before_list)
for val in sorted(root, key=str):
res.extend(serialize(val, None, indent + 1))
else:
str_root = str(root)
if '\n' in str_root:
# we are in a submodel
if rootkey is not None:
res[0] += self.dict_sep
list_root = [(self.indent_str * (indent + 1)) + line
for line in str_root.split('\n')]
res.extend(list_root)
else:
# just a normal key or list entry
try:
res[0] += self.key_sep + str_root
except IndexError:
res = [(self.indent_str * indent) + str_root]
return res
return "\n".join(serialize(model, None, -1))
class TableView(object):
"""A Basic Table Text View
This view performs serialization of data into a basic table with
predefined column names and mappings. Column width is auto-calculated
evenly, column values are automatically truncated accordingly. Values
are centered in the columns.
:param [str] column_names: the headers for each of the columns
:param [str] column_values: the item name to match each column to in
each row
:param str table_prop_name: the name of the property within the model
containing the row models
"""
def __init__(self, column_names, column_values, table_prop_name):
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)
column_headers = "|".join(
"{ch[" + str(n) + "]: ^" + str(self.column_width) + "}"
for n in range(len(column_names))
)
# correct for float-to-int roundoff error
test_fmt = column_headers.format(ch=column_names)
if len(test_fmt) < 72:
column_headers += ' ' * (72 - len(test_fmt))
vert_divider = '-' * 72
self.header_fmt_str = column_headers + "\n" + vert_divider + "\n"
self.row_fmt_str = "|".join(
"{cv[" + str(n) + "]: ^" + str(self.column_width) + "}"
for n in range(len(column_values))
)
def __call__(self, model):
res = self.header_fmt_str.format(ch=self.column_names)
for raw_row in model[self.table_prop_name]:
row = [str(raw_row[prop_name]) for prop_name in self.column_values]
# double format is in case we have roundoff error
res += '{0: <72}\n'.format(self.row_fmt_str.format(cv=row))
return res

View File

@ -0,0 +1,51 @@
# 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
# 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.
"""Text Views With Headers
This package defines several text views with headers
"""
class HeaderView(object):
"""A Text View With a Header
This view simply serializes the model and places the given
header on top.
:param header: the header (can be anything on which str() can be called)
"""
def __init__(self, header):
self.header = header
def __call__(self, model):
return str(self.header) + "\n" + str(model)
class TitledView(HeaderView):
"""A Text View With a Title
This view simply serializes the model, and places
a preformatted header containing the given title
text on top. The title text can be up to 64 characters
long.
:param str title: the title of the view
"""
FORMAT_STR = ('=' * 72) + "\n===={0: ^64}====\n" + ('=' * 72)
def __init__(self, title):
super(TitledView, self).__init__(self.FORMAT_STR.format(title))

View File

@ -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 designate.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 %}"
)

View File

@ -0,0 +1,80 @@
# 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
# 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 thread and stack-trace views
This module provides a collection of views for
visualizing threads, green threads, and stack traces
in human-readable form.
"""
from designate.openstack.common.report.views import jinja_view as jv
class StackTraceView(jv.JinjaView):
"""A Stack Trace View
This view displays stack trace models defined by
:class:`openstack.common.report.models.threading.StackTraceModel`
"""
VIEW_TEXT = (
"{% if root_exception is not none %}"
"Exception: {{ root_exception }}\n"
"------------------------------------\n"
"\n"
"{% endif %}"
"{% for line in lines %}\n"
"{{ line.filename }}:{{ line.line }} in {{ line.name }}\n"
" {% if line.code is not none %}"
"`{{ line.code }}`"
"{% else %}"
"(source not found)"
"{% endif %}\n"
"{% else %}\n"
"No Traceback!\n"
"{% endfor %}"
)
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`
"""
FORMAT_STR = "------{thread_str: ^60}------" + "\n" + "{stack_trace}"
def __call__(self, model):
return self.FORMAT_STR.format(
thread_str=" Green Thread ",
stack_trace=model.stack_trace
)
class ThreadView(object):
"""A Thread Collection View
This view displays a python thread provided by the data
model :class:`openstack.common.report.models.threading.ThreadModel` # noqa
"""
FORMAT_STR = "------{thread_str: ^60}------" + "\n" + "{stack_trace}"
def __call__(self, model):
return self.FORMAT_STR.format(
thread_str=" Thread #{0} ".format(model.thread_id),
stack_trace=model.stack_trace
)

View File

@ -0,0 +1,19 @@
# 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
# 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 basic XML views
This module provides several basic views which serialize
models into XML.
"""

View File

@ -0,0 +1,87 @@
# 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
# 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 generic XML views
This modules defines several basic views for serializing
data to XML. Submodels that have already been serialized
as XML may have their string values marked with `__is_xml__
= True` using :class:`openstack.common.report.utils.StringWithAttrs`
(each of the classes within this module does this automatically,
and non-naive serializers check for this attribute and handle
such strings specially)
"""
import collections as col
import copy
import xml.etree.ElementTree as ET
import six
from designate.openstack.common.report import utils as utils
class KeyValueView(object):
"""A Key-Value XML View
This view performs advanced serialization of a data model
into XML. It first deserializes any values marked as XML so
that they can be properly reserialized later. It then follows
the following rules to perform serialization:
key : text/xml
The tag name is the key name, and the contents are the text or xml
key : Sequence
A wrapper tag is created with the key name, and each item is placed
in an 'item' tag
key : Mapping
A wrapper tag is created with the key name, and the serialize is called
on each key-value pair (such that each key gets its own tag)
:param str wrapper_name: the name of the top-level element
"""
def __init__(self, wrapper_name="model"):
self.wrapper_name = wrapper_name
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_xml__', False):
cpy[key] = ET.fromstring(valstr)
def serialize(rootmodel, rootkeyname):
res = ET.Element(rootkeyname)
if isinstance(rootmodel, col.Mapping):
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 sorted(rootmodel, key=str):
res.append(serialize(val, 'item'))
elif ET.iselement(rootmodel):
res.append(rootmodel)
else:
res.text = str(rootmodel)
return res
str_ = ET.tostring(serialize(cpy,
self.wrapper_name),
encoding="utf-8").decode("utf-8")
res = utils.StringWithAttrs(str_)
res.__is_xml__ = True
return res

View File

@ -28,6 +28,8 @@ from oslo_log import log as logging
from oslo_utils import timeutils from oslo_utils import timeutils
from designate import exceptions from designate import exceptions
from designate.openstack.common.report import guru_meditation_report as gmr
from designate import version as designate_version
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -375,3 +377,7 @@ def cache_result(function):
cache[0] = result cache[0] = result
return result return result
return wrapper return wrapper
def setup_gmr(log_dir=None):
gmr.TextGuruMeditation.setup_autorun(designate_version, log_dir=log_dir)

View File

@ -15,4 +15,26 @@
# under the License. # under the License.
import pbr.version import pbr.version
DESIGNATE_VENDOR = "OpenStack Foundation"
DESIGNATE_PRODUCT = "OpenStack Designate"
version_info = pbr.version.VersionInfo('designate') version_info = pbr.version.VersionInfo('designate')
def vendor_string():
return DESIGNATE_VENDOR
def product_string():
return DESIGNATE_PRODUCT
def package_string():
return None
def version_string_with_package():
if package_string() is None:
return version_info.version_string()
else:
return "%s-%s" % (version_info.version_string(), package_string())

466
doc/source/gmr.rst Normal file
View File

@ -0,0 +1,466 @@
.. _gmr:
=========================
Guru Meditation Reports
=========================
A Guru Meditation Reports(GMR) is gerenated by the Designate services when
service processes receiving SIGUSR1 signal. The report is a general-purpose
debug report for developers and system admins which contains the current state
of a running Designate service process.
Structure of a GMR
==================
Package
Shows information about the package to which this process belongs, including
version information
Threads
Shows stack traces and thread ids for each of the threads within this process
Green Threads
Shows stack traces for each of the green threads within this process (green
threads don't have thread ids)
Processes
Shows information about this process, including pid, ppid, uid and process
state
Configuration
Lists all the configuration options currently accessible via the CONF object
for the current process
Generate a GMR
==============
A GMR can be generated by sending the USR1 signal to any Designate processes.
For example, suppose ``designate-central`` has pid ``15097``, ``kill -USR1
15097`` will trigger a GMR.
If option ``logdir`` has been set in ``designate.conf``, the GMR will be saved
in the folder which ``logdir`` specified. Otherwise, the GMR will be printed to
the stderr.
Reference
=========
For more information about GMR, see `GMR wiki`_.
.. _GMR wiki: https://wiki.openstack.org/wiki/GuruMeditationReport
GMR Example
===========
::
========================================================================
==== Guru Meditation ====
========================================================================
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
========================================================================
==== Package ====
========================================================================
product = OpenStack Designate
vendor = OpenStack Foundation
version = 2015.1
========================================================================
==== Threads ====
========================================================================
------ Thread #140098874533632 ------
/usr/local/lib/python2.7/dist-packages/eventlet/hubs/hub.py:346 in run
`self.wait(sleep_time)`
/usr/local/lib/python2.7/dist-packages/eventlet/hubs/poll.py:85 in wait
`presult = self.do_poll(seconds)`
/usr/local/lib/python2.7/dist-packages/eventlet/hubs/epolls.py:62 in do_poll
`return self.poll.poll(seconds)`
========================================================================
==== Green Threads ====
========================================================================
------ Green Thread ------
/usr/local/lib/python2.7/dist-packages/eventlet/greenthread.py:214 in main
`result = function(*args, **kwargs)`
/opt/stack/designate/designate/openstack/common/service.py:492 in run_service
`done.wait()`
/usr/local/lib/python2.7/dist-packages/eventlet/event.py:121 in wait
`return hubs.get_hub().switch()`
/usr/local/lib/python2.7/dist-packages/eventlet/hubs/hub.py:294 in switch
`return self.greenlet.switch()`
------ Green Thread ------
/usr/local/lib/python2.7/dist-packages/eventlet/greenthread.py:214 in main
`result = function(*args, **kwargs)`
/usr/local/lib/python2.7/dist-packages/oslo_utils/excutils.py:95 in inner_func
`return infunc(*args, **kwargs)`
/usr/local/lib/python2.7/dist-packages/oslo_messaging/_executors/impl_eventlet.py:96 in _executor_thread
`incoming = self.listener.poll()`
/usr/local/lib/python2.7/dist-packages/oslo_messaging/_drivers/amqpdriver.py:121 in poll
`self.conn.consume(limit=1, timeout=timeout)`
/usr/local/lib/python2.7/dist-packages/oslo_messaging/_drivers/impl_rabbit.py:867 in consume
`six.next(it)`
/usr/local/lib/python2.7/dist-packages/oslo_messaging/_drivers/impl_rabbit.py:782 in iterconsume
`yield self.ensure(_error_callback, _consume)`
/usr/local/lib/python2.7/dist-packages/oslo_messaging/_drivers/impl_rabbit.py:688 in ensure
`ret, channel = autoretry_method()`
/usr/local/lib/python2.7/dist-packages/kombu/connection.py:436 in _ensured
`return fun(*args, **kwargs)`
/usr/local/lib/python2.7/dist-packages/kombu/connection.py:508 in __call__
`return fun(*args, channel=channels[0], **kwargs), channels[0]`
/usr/local/lib/python2.7/dist-packages/oslo_messaging/_drivers/impl_rabbit.py:675 in execute_method
`method()`
/usr/local/lib/python2.7/dist-packages/oslo_messaging/_drivers/impl_rabbit.py:774 in _consume
`return self.connection.drain_events(timeout=poll_timeout)`
/usr/local/lib/python2.7/dist-packages/kombu/connection.py:275 in drain_events
`return self.transport.drain_events(self.connection, **kwargs)`
/usr/local/lib/python2.7/dist-packages/kombu/transport/pyamqp.py:91 in drain_events
`return connection.drain_events(**kwargs)`
/usr/local/lib/python2.7/dist-packages/amqp/connection.py:302 in drain_events
`chanmap, None, timeout=timeout,`
/usr/local/lib/python2.7/dist-packages/amqp/connection.py:365 in _wait_multiple
`channel, method_sig, args, content = read_timeout(timeout)`
/usr/local/lib/python2.7/dist-packages/amqp/connection.py:336 in read_timeout
`return self.method_reader.read_method()`
/usr/local/lib/python2.7/dist-packages/amqp/method_framing.py:186 in read_method
`self._next_method()`
/usr/local/lib/python2.7/dist-packages/amqp/method_framing.py:107 in _next_method
`frame_type, channel, payload = read_frame()`
/usr/local/lib/python2.7/dist-packages/amqp/transport.py:154 in read_frame
`frame_header = read(7, True)`
/usr/local/lib/python2.7/dist-packages/amqp/transport.py:277 in _read
`s = recv(n - len(rbuf))`
/usr/local/lib/python2.7/dist-packages/eventlet/greenio/base.py:326 in recv
`timeout_exc=socket.timeout("timed out"))`
/usr/local/lib/python2.7/dist-packages/eventlet/greenio/base.py:201 in _trampoline
`mark_as_closed=self._mark_as_closed)`
/usr/local/lib/python2.7/dist-packages/eventlet/hubs/__init__.py:162 in trampoline
`return hub.switch()`
/usr/local/lib/python2.7/dist-packages/eventlet/hubs/hub.py:294 in switch
`return self.greenlet.switch()`
------ Green Thread ------
/usr/local/bin/designate-central:10 in <module>
`sys.exit(main())`
/opt/stack/designate/designate/cmd/central.py:37 in main
`service.wait()`
/opt/stack/designate/designate/service.py:356 in wait
`_launcher.wait()`
/opt/stack/designate/designate/openstack/common/service.py:187 in wait
`status, signo = self._wait_for_exit_or_signal(ready_callback)`
/opt/stack/designate/designate/openstack/common/service.py:170 in _wait_for_exit_or_signal
`super(ServiceLauncher, self).wait()`
/opt/stack/designate/designate/openstack/common/service.py:133 in wait
`self.services.wait()`
/opt/stack/designate/designate/openstack/common/service.py:473 in wait
`self.tg.wait()`
/opt/stack/designate/designate/openstack/common/threadgroup.py:145 in wait
`x.wait()`
/opt/stack/designate/designate/openstack/common/threadgroup.py:47 in wait
`return self.thread.wait()`
/usr/local/lib/python2.7/dist-packages/eventlet/greenthread.py:175 in wait
`return self._exit_event.wait()`
/usr/local/lib/python2.7/dist-packages/eventlet/event.py:121 in wait
`return hubs.get_hub().switch()`
/usr/local/lib/python2.7/dist-packages/eventlet/hubs/hub.py:294 in switch
`return self.greenlet.switch()`
------ Green Thread ------
No Traceback!
========================================================================
==== Processes ====
========================================================================
Process 15097 (under 7312) [ run by: stanzgy (1000), state: running ]
========================================================================
==== Configuration ====
========================================================================
backend:agent:bind9:
query-destination = 127.0.0.1
rndc-config-file = None
rndc-host = 127.0.0.1
rndc-key-file = None
rndc-port = 953
zone-file-path = /opt/stack/data/designate/zones
backend:bind9:
masters =
127.0.0.1:5354
rndc-config-file = None
rndc-host = 127.0.0.1
rndc-key-file = None
rndc-port = 953
server_ids =
backend:fake:
masters =
127.0.0.1:5354
server_ids =
backend:powerdns:
backend = sqlalchemy
connection = ***
connection_debug = 0
connection_trace = False
db_inc_retry_interval = True
db_max_retries = 20
db_max_retry_interval = 10
db_retry_interval = 1
idle_timeout = 3600
masters =
10.180.64.117:5354
max_overflow = None
max_pool_size = None
max_retries = 10
min_pool_size = 1
mysql_sql_mode = TRADITIONAL
pool_timeout = None
retry_interval = 10
server_ids =
f26e0b32-736f-4f0a-831b-039a415c481e
slave_connection = ***
sqlite_db = oslo.sqlite
sqlite_synchronous = True
use_db_reconnect = False
backend:powerdns:f26e0b32-736f-4f0a-831b-039a415c481e:
backend = None
connection = ***
connection_debug = None
connection_trace = None
db_inc_retry_interval = None
db_max_retries = None
db_max_retry_interval = None
db_retry_interval = None
host = 10.180.64.117
idle_timeout = None
masters = None
max_overflow = None
max_pool_size = None
max_retries = None
min_pool_size = None
mysql_sql_mode = None
pool_timeout = None
port = 53
retry_interval = None
slave_connection = ***
sqlite_db = None
sqlite_synchronous = None
tsig-key = None
use_db_reconnect = None
default:
allowed_remote_exmods =
backdoor_port = None
backlog = 4096
central-topic = central
config-dir = None
config-file =
/etc/designate/designate.conf
control_exchange = designate
debug = True
default-soa-expire = 86400
default-soa-minimum = 3600
default-soa-refresh = 3600
default-soa-retry = 600
default-ttl = 3600
default_log_levels =
amqp=WARN
amqplib=WARN
boto=WARN
eventlet.wsgi.server=WARN
keystone=INFO
keystonemiddleware.auth_token=INFO
oslo.messaging=WARN
sqlalchemy=WARN
stevedore=WARN
suds=INFO
fatal_deprecations = False
host = cns-dev2
instance_format = [instance: %(uuid)s]
instance_uuid_format = [instance: %(uuid)s]
log-config-append = None
log-date-format = %Y-%m-%d %H:%M:%S
log-dir = /opt/stack/logs/designate
log-file = None
log-format = None
logging_context_format_string = %(asctime)s.%(msecs)03d %(color)s%(levelname)s %(name)s [%(request_id)s %(user)s %(tenant)s%(color)s] %(instance)s%(color)s%(message)s
logging_debug_format_suffix = from (pid=%(process)d) %(funcName)s %(pathname)s:%(lineno)d
logging_default_format_string = %(asctime)s.%(msecs)03d %(color)s%(levelname)s %(name)s [-%(color)s] %(instance)s%(color)s%(message)s
logging_exception_prefix = %(color)s%(asctime)s.%(msecs)03d TRACE %(name)s %(instance)s
mdns-topic = mdns
network_api = neutron
notification_driver =
notification_topics =
notifications
policy_default_rule = default
policy_dirs =
policy.d
policy_file = /etc/designate/policy.json
pool-manager-topic = pool_manager
publish_errors = False
pybasedir = /opt/stack/designate
quota-domain-records = 500
quota-domain-recordsets = 500
quota-domains = 10
quota-driver = storage
quota-recordset-records = 20
root-helper = sudo designate-rootwrap /etc/designate/rootwrap.conf
rpc_backend = rabbit
rpc_thread_pool_size = 64
state-path = /opt/stack/data/designate
syslog-log-facility = LOG_USER
tcp_keepidle = 600
transport_url = None
use-syslog = False
use-syslog-rfc-format = False
use_stderr = True
verbose = True
network_api:neutron:
admin_password = ***
admin_tenant_name = None
admin_username = None
auth_strategy = keystone
auth_url = None
ca_certificates_file = None
endpoint_type = publicURL
endpoints = None
insecure = False
timeout = 30
oslo_concurrency:
disable_process_locking = False
lock_path = None
oslo_messaging_rabbit:
amqp_auto_delete = False
amqp_durable_queues = False
fake_rabbit = False
kombu_reconnect_delay = 1.0
kombu_ssl_ca_certs =
kombu_ssl_certfile =
kombu_ssl_keyfile =
kombu_ssl_version =
rabbit_ha_queues = False
rabbit_host = localhost
rabbit_hosts =
127.0.0.1
rabbit_login_method = AMQPLAIN
rabbit_max_retries = 0
rabbit_password = ***
rabbit_port = 5672
rabbit_retry_backoff = 2
rabbit_retry_interval = 1
rabbit_use_ssl = False
rabbit_userid = stackrabbit
rabbit_virtual_host = /
rpc_conn_pool_size = 30
proxy:
http_proxy = None
https_proxy = None
no_proxy =
service:central:
default_pool_id = 794ccc2c-d751-44fe-b57f-8894c9f5c842
enabled-notification-handlers =
managed_resource_email = hostmaster@example.com
managed_resource_tenant_id = None
max_domain_name_len = 255
max_recordset_name_len = 255
min_ttl = None
storage-driver = sqlalchemy
workers = None
service:pool_manager:
backends =
powerdns
cache-driver = sqlalchemy
enable-recovery-timer = True
enable-sync-timer = True
periodic-recovery-interval = 120
periodic-sync-interval = 300
periodic-sync-seconds = None
poll-delay = 1
poll-max-retries = 3
poll-retry-interval = 2
poll-timeout = 30
pool-id = 794ccc2c-d751-44fe-b57f-8894c9f5c842
threshold-percentage = 100
workers = None
ssl:
ca_file = None
cert_file = None
key_file = None
storage:sqlalchemy:
backend = sqlalchemy
connection = ***
connection_debug = 0
connection_trace = False
db_inc_retry_interval = True
db_max_retries = 20
db_max_retry_interval = 10
db_retry_interval = 1
idle_timeout = 3600
max_overflow = None
max_pool_size = None
max_retries = 10
min_pool_size = 1
mysql_sql_mode = TRADITIONAL
pool_timeout = None
retry_interval = 10
slave_connection = ***
sqlite_db = oslo.sqlite
sqlite_synchronous = True
use_db_reconnect = False

View File

@ -50,6 +50,7 @@ Reference Documentation
backends backends
integrations integrations
tempest tempest
gmr
Source Documentation Source Documentation
==================== ====================

View File

@ -7,6 +7,13 @@ script=tools/install_venv_common.py
module=memorycache module=memorycache
module=policy module=policy
module=service module=service
module=report
module=report.generators
module=report.models
module=report.views
module=report.views.xml
module=report.views.json
module=report.views.text
# Modules needed for the deprecated oslo.wsgi we're still using # Modules needed for the deprecated oslo.wsgi we're still using
module=sslutils module=sslutils

View File

@ -37,3 +37,4 @@ oslo.i18n>=1.3.0 # Apache-2.0
oslo.context>=0.2.0 # Apache-2.0 oslo.context>=0.2.0 # Apache-2.0
Werkzeug>=0.7 # BSD License Werkzeug>=0.7 # BSD License
python-memcached>=1.48 python-memcached>=1.48
psutil>=1.1.1,<2.0.0