Add entry point for OSProfiler, that display traces

This patch implements basic client code for OSProfiler, which contains:
 * osprofiler.cmd.cliutils - module with utils for client code
 * osprofiler.cmd.commands - module with all commands. Each group of
   commands should inherit from `osprofiler.cmd.commands.BaseCommand` class
   (implemented commands: `results show`)
 * osprofiler.cmd.exc - module for clients exceptions
   (implemented exceptions: CommandError)
 * osprofiler.cmd.shell - module with basic shell class(`OSProfilerShell`),
   which append ceilometer and identity arguments groups, discover all cli
   commands in `osprofiler.cmd.commands`.
 * osprofiler.cmd.template.html - html-template for command
   "osprofiler trace show"

Change-Id: If4bd50658c594793fe97e8ba1c9867694aa46ff4
This commit is contained in:
Andrey Kurilin 2014-07-21 12:10:56 +03:00
parent b2c9b86ad7
commit 42ec4ccda1
11 changed files with 860 additions and 0 deletions

View File

@ -13,8 +13,19 @@
# License for the specific language governing permissions and limitations
# under the License.
import os
from six.moves import configparser
from osprofiler import _utils as utils
utils.import_modules_from_package("osprofiler._notifiers")
_conf = configparser.ConfigParser()
_conf.read(os.path.join(
os.path.dirname(os.path.dirname(__file__)), 'setup.cfg'))
try:
__version__ = _conf.get('metadata', 'version')
except (configparser.NoOptionError, configparser.NoSectionError):
__version__ = None

View File

View File

@ -0,0 +1,57 @@
# Copyright 2014 Mirantis Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
def env(*args, **kwargs):
"""Returns the first environment variable set.
If all are empty, defaults to '' or keyword arg `default`.
"""
for arg in args:
value = os.environ.get(arg)
if value:
return value
return kwargs.get('default', '')
def arg(*args, **kwargs):
"""Decorator for CLI args.
Example:
>>> @arg("name", help="Name of the new entity")
... def entity_create(args):
... pass
"""
def _decorator(func):
add_arg(func, *args, **kwargs)
return func
return _decorator
def add_arg(func, *args, **kwargs):
"""Bind CLI arguments to a shell.py `do_foo` function."""
if not hasattr(func, 'arguments'):
func.arguments = []
# NOTE(sirp): avoid dups that can occur when the module is shared across
# tests.
if (args, kwargs) not in func.arguments:
# Because of the semantics of decorator composition if we just append
# to the options list positional options will appear to be backwards.
func.arguments.insert(0, (args, kwargs))

View File

@ -0,0 +1,84 @@
# Copyright 2014 Mirantis Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import os
from osprofiler.cmd import cliutils
from osprofiler.cmd import exc
from osprofiler.parsers import ceilometer as ceiloparser
class BaseCommand(object):
group_name = None
class TraceCommands(BaseCommand):
group_name = "trace"
@cliutils.arg('trace_id', help='trace id')
@cliutils.arg('--json', dest='use_json', action='store_true',
help='show trace in JSON')
@cliutils.arg('--html', dest='use_html', action='store_true',
help='show trace in HTML')
@cliutils.arg('--out', dest='file_name', help='save output in file')
def show(self, args):
"""Displays trace-results by given trace id in HTML or JSON format."""
try:
import ceilometerclient.client
import ceilometerclient.exc
import ceilometerclient.shell
except ImportError:
raise ImportError(
"To use this command, you should install 'ceilometerclient' "
"manually. Use command:\n 'pip install ceilometerclient'.")
try:
client = ceilometerclient.client.get_client(
args.ceilometer_api_version, **args.__dict__)
notifications = ceiloparser.get_notifications(
client, args.trace_id)
except Exception as e:
if hasattr(e, 'http_status') and e.http_status == 401:
msg = "Invalid OpenStack Identity credentials."
else:
msg = "Something has gone wrong. See logs for more details."
raise exc.CommandError(msg)
if not notifications:
msg = ("Trace with UUID %s not found. "
"There are 2 possible reasons: \n"
" 1) You are using not admin credentials\n"
" 2) You specified wrong trace id" % args.trace_id)
raise exc.CommandError(msg)
parsed_notifications = ceiloparser.parse_notifications(notifications)
if args.use_json:
output = json.dumps(parsed_notifications)
elif args.use_html:
with open(os.path.join(os.path.dirname(__file__),
"template.html")) as html_template:
output = html_template.read().replace(
"$DATA", json.dumps(parsed_notifications))
else:
raise exc.CommandError("You should choose one of the following "
"output-formats: --json or --html.")
if args.file_name:
with open(args.file_name, 'w+') as output_file:
output_file.write(output)
else:
print (output)

24
osprofiler/cmd/exc.py Normal file
View File

@ -0,0 +1,24 @@
# Copyright 2014 Mirantis Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
class CommandError(Exception):
"""Invalid usage of CLI."""
def __init__(self, message=None):
self.message = message
def __str__(self):
return self.message or self.__class__.__doc__

243
osprofiler/cmd/shell.py Normal file
View File

@ -0,0 +1,243 @@
# Copyright 2014 Mirantis Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Command-line interface to the OpenStack Profiler.
"""
import inspect
import sys
import argparse
import osprofiler
from osprofiler.cmd import cliutils
from osprofiler.cmd import commands
from osprofiler.cmd import exc
class OSProfilerShell(object):
def __init__(self, argv):
args = self._get_base_parser().parse_args(argv)
if not (args.os_auth_token and args.ceilometer_url):
if not args.os_username:
raise exc.CommandError(
"You must provide a username via either --os-username or "
"via env[OS_USERNAME]")
if not args.os_password:
raise exc.CommandError(
"You must provide a password via either --os-password or "
"via env[OS_PASSWORD]")
if self._no_project_and_domain_set(args):
# steer users towards Keystone V3 API
raise exc.CommandError(
"You must provide a project_id via either --os-project-id "
"or via env[OS_PROJECT_ID] and a domain_name via either "
"--os-user-domain-name or via env[OS_USER_DOMAIN_NAME] or "
"a domain_id via either --os-user-domain-id or via "
"env[OS_USER_DOMAIN_ID]")
if not args.os_auth_url:
raise exc.CommandError(
"You must provide an auth url via either --os-auth-url or "
"via env[OS_AUTH_URL]")
args.func(args)
def _get_base_parser(self):
parser = argparse.ArgumentParser(
prog="osprofiler",
description=__doc__.strip(),
add_help=True
)
parser.add_argument('-v', '--version',
action='version',
version=osprofiler.__version__)
self._append_ceilometer_args(parser)
self._append_identity_args(parser)
self._append_subcommands(parser)
return parser
def _append_ceilometer_args(self, parent_parser):
parser = parent_parser.add_argument_group('ceilometer')
parser.add_argument(
'--ceilometer-url', default=cliutils.env('CEILOMETER_URL'),
help='Defaults to env[CEILOMETER_URL].')
parser.add_argument(
'--ceilometer-api-version',
default=cliutils.env('CEILOMETER_API_VERSION', default='2'),
help='Defaults to env[CEILOMETER_API_VERSION] or 2.')
def _append_identity_args(self, parent_parser):
# FIXME(fabgia): identity related parameters should be passed by the
# Keystone client itself to avoid constant update in all the services
# clients. When this fix is merged this method can be made obsolete.
# Bug: https://bugs.launchpad.net/python-keystoneclient/+bug/1332337
parser = parent_parser.add_argument_group('identity')
parser.add_argument('-k', '--insecure',
default=False,
action='store_true',
help="Explicitly allow osprofiler to "
"perform \"insecure\" SSL (https) requests. "
"The server's certificate will "
"not be verified against any certificate "
"authorities. This option should be used with "
"caution.")
# User related options
parser.add_argument('--os-username',
default=cliutils.env('OS_USERNAME'),
help='Defaults to env[OS_USERNAME].')
parser.add_argument('--os-user-id',
default=cliutils.env('OS_USER_ID'),
help='Defaults to env[OS_USER_ID].')
parser.add_argument('--os-password',
default=cliutils.env('OS_PASSWORD'),
help='Defaults to env[OS_PASSWORD].')
# Domain related options
parser.add_argument('--os-user-domain-id',
default=cliutils.env('OS_USER_DOMAIN_ID'),
help='Defaults to env[OS_USER_DOMAIN_ID].')
parser.add_argument('--os-user-domain-name',
default=cliutils.env('OS_USER_DOMAIN_NAME'),
help='Defaults to env[OS_USER_DOMAIN_NAME].')
parser.add_argument('--os-project-domain-id',
default=cliutils.env('OS_PROJECT_DOMAIN_ID'),
help='Defaults to env[OS_PROJECT_DOMAIN_ID].')
parser.add_argument('--os-project-domain-name',
default=cliutils.env('OS_PROJECT_DOMAIN_NAME'),
help='Defaults to env[OS_PROJECT_DOMAIN_NAME].')
# Project V3 or Tenant V2 related options
parser.add_argument('--os-project-id',
default=cliutils.env('OS_PROJECT_ID'),
help='Another way to specify tenant ID. '
'This option is mutually exclusive with '
' --os-tenant-id. '
'Defaults to env[OS_PROJECT_ID].')
parser.add_argument('--os-project-name',
default=cliutils.env('OS_PROJECT_NAME'),
help='Another way to specify tenant name. '
'This option is mutually exclusive with '
' --os-tenant-name. '
'Defaults to env[OS_PROJECT_NAME].')
parser.add_argument('--os-tenant-id',
default=cliutils.env('OS_TENANT_ID'),
help='This option is mutually exclusive with '
' --os-project-id. '
'Defaults to env[OS_PROJECT_ID].')
parser.add_argument('--os-tenant-name',
default=cliutils.env('OS_TENANT_NAME'),
help='Defaults to env[OS_TENANT_NAME].')
# Auth related options
parser.add_argument('--os-auth-url',
default=cliutils.env('OS_AUTH_URL'),
help='Defaults to env[OS_AUTH_URL].')
parser.add_argument('--os-auth-token',
default=cliutils.env('OS_AUTH_TOKEN'),
help='Defaults to env[OS_AUTH_TOKEN].')
parser.add_argument('--os-cacert',
metavar='<ca-certificate-file>',
dest='os_cacert',
default=cliutils.env('OS_CACERT'),
help='Path of CA TLS certificate(s) used to verify'
' the remote server\'s certificate. Without this '
'option ceilometer looks for the default system CA'
' certificates.')
parser.add_argument('--os-cert',
help='Path of certificate file to use in SSL '
'connection. This file can optionally be '
'prepended with the private key.')
parser.add_argument('--os-key',
help='Path of client key to use in SSL '
'connection. This option is not necessary '
'if your key is prepended to your cert file.')
# Service Catalog related options
parser.add_argument('--os-service-type',
default=cliutils.env('OS_SERVICE_TYPE'),
help='Defaults to env[OS_SERVICE_TYPE].')
parser.add_argument('--os-endpoint-type',
default=cliutils.env('OS_ENDPOINT_TYPE'),
help='Defaults to env[OS_ENDPOINT_TYPE].')
parser.add_argument('--os-region-name',
default=cliutils.env('OS_REGION_NAME'),
help='Defaults to env[OS_REGION_NAME].')
def _append_subcommands(self, parent_parser):
subcommands = parent_parser.add_subparsers(help='<subcommands>')
for group_cls in commands.BaseCommand.__subclasses__():
group_parser = subcommands.add_parser(group_cls.group_name)
subcommand_parser = group_parser.add_subparsers()
for name, callback in inspect.getmembers(
group_cls(), predicate=inspect.ismethod):
command = name.replace('_', '-')
desc = callback.__doc__ or ''
help_message = desc.strip().split('\n')[0]
arguments = getattr(callback, 'arguments', [])
command_parser = subcommand_parser.add_parser(
command, help=help_message, description=desc)
for (args, kwargs) in arguments:
command_parser.add_argument(*args, **kwargs)
command_parser.set_defaults(func=callback)
def _no_project_and_domain_set(self, args):
if not (args.os_project_id or (args.os_project_name and
(args.os_user_domain_name or args.os_user_domain_id)) or
(args.os_tenant_id or args.os_tenant_name)):
return True
else:
return False
def main(args=None):
if args is None:
args = sys.argv[1:]
try:
OSProfilerShell(args)
except exc.CommandError as e:
print (e.message)
return 1
if __name__ == "__main__":
main()

View File

@ -0,0 +1,209 @@
<!doctype html>
<html ng-app="Application">
<head>
<script>
var OSProfilerData = $DATA
</script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.10/angular.min.js"></script>
<script src="https://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.11.0.js"></script>
<style>
.trace {
min-width: 900px;
width: 100%;
}
.trace tr.active-true {
background-color: #D9EDF7!important;
}
.trace tr td {
width: 14%;
white-space: nowrap;
padding: 2px;
border-right: 1px solid #eee;
}
.trace tr td.details {
width: 10%;
padding-right: 20px;
}
.trace.cursor_pointer_on_hover {
cursor: pointer;
}
.trace .level {
width: 10%;
font-weight: bold;
}
.bold {
font-weight: bold;
}
.duration {
width: 25px;
margin: 0px;
padding: 0px;
background-color: #c6eff3;
border-radius: 4px;
font-size: 10px;
}
.duration div{
padding-top: 4px;
padding-bottom: 4px;
text-align: center;
}
</style>
<script type="text/ng-template" id="tree_item_renderer.html">
<div ng-init="hide_children=false">
<table class="trace cursor_pointer_on_hover">
<tr class="active-{{hover}}" ng-init="hover=false" ng-mouseenter="hover=true" ng-mouseleave="hover=false">
<td class="level" style="padding-left:{{data.level * 5}}px;">
<button type="button" class="btn btn-default btn-xs" ng-disabled="data.is_leaf" ng-click="hide_children=!hide_children">
<span class="glyphicon glyphicon-{{ (data.is_leaf) ? 'cloud' : ((hide_children) ? 'plus': 'minus')}}"></span>
{{data.level || 0}}
</button>
</td>
<td ng-click="display(data);" class="text-center">
<div class="duration" style="width: {{get_width(data)}}%; margin-left: {{get_started(data)}}%">
<div>{{data.info.finished - data.info.started}} ms</div>
</div>
</td>
<td ng-click="display(data);" class="{{ is_important(data) ? 'bold' : ''}} text-right" > {{data.info.name}} </td>
<td ng-click="display(data);"> {{data.info.project || "n/a"}}</td>
<td ng-click="display(data);"> {{data.info.service || "n/a" }} </td>
<td ng-click="display(data);"> {{data.info.host || "n/a"}} </td>
<td class="details">
<a href="#" ng-click="display(data);"> Details </a>
</td>
</tr>
</table>
<div ng-hide="hide_children">
<div ng-repeat="data in data.children" ng-include="'tree_item_renderer.html'"> </div>
</div>
</div>
</script>
<script>
angular.module("Application", ['ui.bootstrap']);
function ProfilerCtlr($scope, $modal) {
var convert_input = function(input, level){
level = (level) ? level : 0;
input.level = level;
input.is_leaf = !input.children.length
for (var i=0; i < input.children.length; i++)
convert_input(input.children[i], level + 1);
return input;
}
$scope.get_width = function(data){
var full_duration = $scope.tree[0].info.finished;
var duration = (data.info.finished - data.info.started) * 100.0 / full_duration;
return (duration >= 0.5) ? duration : 0.5;
}
$scope.get_started = function(data) {
var full_duration = $scope.tree[0].info.finished;
return data.info.started * 100.0 / full_duration;
}
$scope.is_important = function(data) {
return ["total", "wsgi", "rpc"].indexOf(data.info.name) != -1;
}
$scope.display = function(data){
var info = angular.copy(data.info);
var metadata = {};
angular.forEach(info, function(value, key) {
var parts = key.split(".");
if (parts[0] == "info"){
if (parts.length != 2){
this[key] = value;
}
else{
var group_name = parts[1].split(":");
if (group_name.length == 2){
if (!(group_name[0] in this))
this[group_name[0]] = {};
this[group_name[0]][group_name[1]] = value;
}
}
};
}, metadata);
info["metadata"] = "<pre>" + JSON.stringify(metadata, "", 4) + "</pre>"
var trace_data = "<div class='row'>"
columns = ["name", "project", "service", "host", "started",
"finished", "host", "metadata"];
for (var i = 0; i < columns.length; i++){
trace_data += "<div class='col-md-2 text-right text-capitalize'><strong>" + columns[i] + " </strong></div>";
trace_data += "<div class='col-md-10 text-left'>" + info[columns[i]] + "</div>";
}
trace_data += "</div>";
var output = (
'<div class="modal-header"> Trace Point Details </div>' +
'<div class="modal-body">' + trace_data + '</div>' +
'<div class="modal-footer"> <span class="glyphicon glyphicon-cloud </div>'
)
var modal_instance = $modal.open({
"template": output,
"size": "lg"
});
}
$scope.tree = [convert_input(OSProfilerData)];
}
</script>
</head>
<body>
<div ng-controller="ProfilerCtlr">
<table>
</table>
<table class="trace">
<tr class="bold text-left" style="border-bottom: solid 1px gray">
<td class="level">Levels</td>
<td>Duration</td>
<td class="text-right">Type</td>
<td>Project</td>
<td>Service</td>
<td>Host</td>
<td class="details">Details</td>
</tr>
</table>
<div ng-repeat="data in tree" ng-include="'tree_item_renderer.html'"></div>
</div>
</body>
</html>

View File

@ -1 +1,3 @@
argparse
six>=1.7.0
WebOb>=1.2.3

View File

@ -31,3 +31,7 @@ setup-hooks =
all_files = 1
build-dir = doc/build
source-dir = doc/source
[entry_points]
console_scripts =
osprofiler = osprofiler.cmd.shell:main

0
tests/cmd/__init__.py Normal file
View File

226
tests/cmd/test_shell.py Normal file
View File

@ -0,0 +1,226 @@
# Copyright 2014 Mirantis Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import os
import sys
import mock
import six
from osprofiler.cmd import exc
from osprofiler.cmd import shell
from tests import test
class ShellTestCase(test.TestCase):
def setUp(self):
super(ShellTestCase, self).setUp()
self.old_environment = os.environ.copy()
os.environ = {
'OS_USERNAME': 'username',
'OS_USER_ID': 'user_id',
'OS_PASSWORD': 'password',
'OS_USER_DOMAIN_ID': 'user_domain_id',
'OS_USER_DOMAIN_NAME': 'user_domain_name',
'OS_PROJECT_DOMAIN_ID': 'project_domain_id',
'OS_PROJECT_DOMAIN_NAME': 'project_domain_name',
'OS_PROJECT_ID': 'project_id',
'OS_PROJECT_NAME': 'project_name',
'OS_TENANT_ID': 'tenant_id',
'OS_TENANT_NAME': 'tenant_name',
'OS_AUTH_URL': 'http://127.0.0.1:5000/v3/',
'OS_AUTH_TOKEN': 'pass',
'OS_CACERT': '/path/to/cacert',
'OS_SERVICE_TYPE': 'service_type',
'OS_ENDPOINT_TYPE': 'public',
'OS_REGION_NAME': 'test'
}
self.ceiloclient = mock.MagicMock()
sys.modules['ceilometerclient'] = self.ceiloclient
self.addCleanup(sys.modules.pop, 'ceilometerclient', None)
ceilo_modules = ['client', 'exc', 'shell']
for module in ceilo_modules:
sys.modules['ceilometerclient.%s' % module] = getattr(
self.ceiloclient, module)
self.addCleanup(
sys.modules.pop, 'ceilometerclient.%s' % module, None)
def tearDown(self):
super(ShellTestCase, self).tearDown()
os.environ = self.old_environment
@mock.patch("sys.stdout", six.StringIO())
@mock.patch("osprofiler.cmd.shell.OSProfilerShell")
def test_shell_main(self, mock_shell):
mock_shell.side_effect = exc.CommandError('some_message')
shell.main()
self.assertEqual('some_message\n', sys.stdout.getvalue())
def run_command(self, cmd):
shell.OSProfilerShell(cmd.split())
def _test_with_command_error(self, cmd, expected_message):
try:
self.run_command(cmd)
except exc.CommandError as actual_error:
self.assertEqual(str(actual_error), expected_message)
else:
raise ValueError(
'Expected: `osprofiler.cmd.exc.CommandError` is raised with '
'message: "%s".' % expected_message)
def test_username_is_not_presented(self):
os.environ.pop("OS_USERNAME")
msg = ("You must provide a username via either --os-username or "
"via env[OS_USERNAME]")
self._test_with_command_error("trace show fake-uuid", msg)
def test_password_is_not_presented(self):
os.environ.pop("OS_PASSWORD")
msg = ("You must provide a password via either --os-password or "
"via env[OS_PASSWORD]")
self._test_with_command_error("trace show fake-uuid", msg)
def test_auth_url(self):
os.environ.pop("OS_AUTH_URL")
msg = ("You must provide an auth url via either --os-auth-url or "
"via env[OS_AUTH_URL]")
self._test_with_command_error("trace show fake-uuid", msg)
def test_no_project_and_domain_set(self):
os.environ.pop("OS_PROJECT_ID")
os.environ.pop("OS_PROJECT_NAME")
os.environ.pop("OS_TENANT_ID")
os.environ.pop("OS_TENANT_NAME")
os.environ.pop("OS_USER_DOMAIN_ID")
os.environ.pop("OS_USER_DOMAIN_NAME")
msg = ("You must provide a project_id via either --os-project-id or "
"via env[OS_PROJECT_ID] and a domain_name via either "
"--os-user-domain-name or via env[OS_USER_DOMAIN_NAME] or a "
"domain_id via either --os-user-domain-id or via "
"env[OS_USER_DOMAIN_ID]")
self._test_with_command_error("trace show fake-uuid", msg)
def test_trace_show_ceilometrclient_is_missed(self):
sys.modules['ceilometerclient'] = None
sys.modules['ceilometerclient.client'] = None
sys.modules['ceilometerclient.exc'] = None
sys.modules['ceilometerclient.shell'] = None
self.assertRaises(ImportError, shell.main,
'trace show fake_uuid'.split())
def test_trace_show_unauthorized(self):
class FakeHTTPUnauthorized(Exception):
http_status = 401
self.ceiloclient.client.get_client.side_effect = FakeHTTPUnauthorized
msg = "Invalid OpenStack Identity credentials."
self._test_with_command_error("trace show fake_id", msg)
def test_trace_show_unknown_error(self):
class FakeException(Exception):
pass
self.ceiloclient.client.get_client.side_effect = FakeException
msg = "Something has gone wrong. See logs for more details."
self._test_with_command_error("trace show fake_id", msg)
@mock.patch("osprofiler.parsers.ceilometer.get_notifications")
@mock.patch("osprofiler.parsers.ceilometer.parse_notifications")
def test_trace_show_no_selected_format(self, mock_notifications, mock_get):
mock_get.return_value = "some_notificatios"
msg = ("You should choose one of the following output-formats: "
"--json or --html.")
self._test_with_command_error("trace show fake_id", msg)
@mock.patch("osprofiler.parsers.ceilometer.get_notifications")
def test_trace_show_trace_id_not_found(self, mock_get):
mock_get.return_value = None
fake_trace_id = "fake_id"
msg = ("Trace with UUID %s not found. There are 2 possible reasons: \n"
" 1) You are using not admin credentials\n"
" 2) You specified wrong trace id" % fake_trace_id)
self._test_with_command_error("trace show %s" % fake_trace_id, msg)
@mock.patch("sys.stdout", six.StringIO())
@mock.patch("osprofiler.parsers.ceilometer.get_notifications")
@mock.patch("osprofiler.parsers.ceilometer.parse_notifications")
def test_trace_show_in_json(self, mock_notifications, mock_get):
mock_get.return_value = "some notification"
notifications = {
'info': {
'started': 0, 'finished': 0, 'name': 'total'}, 'children': []}
mock_notifications.return_value = notifications
self.run_command("trace show fake_id --json")
self.assertEqual("%s\n" % json.dumps(notifications),
sys.stdout.getvalue())
@mock.patch("sys.stdout", six.StringIO())
@mock.patch("osprofiler.parsers.ceilometer.get_notifications")
@mock.patch("osprofiler.parsers.ceilometer.parse_notifications")
def test_trace_show_in_html(self, mock_notifications, mock_get):
mock_get.return_value = "some notification"
notifications = {
'info': {
'started': 0, 'finished': 0, 'name': 'total'}, 'children': []}
mock_notifications.return_value = notifications
#NOTE(akurilin): to simplify assert statement, html-template should be
# replaced.
html_template = (
"A long time ago in a galaxy far, far away..."
" some_data = $DATA"
"It is a period of civil war. Rebel"
"spaceships, striking from a hidden"
"base, have won their first victory"
"against the evil Galactic Empire.")
with mock.patch("osprofiler.cmd.commands.open",
mock.mock_open(read_data=html_template), create=True):
self.run_command("trace show fake_id --html")
self.assertEqual("A long time ago in a galaxy far, far away..."
" some_data = %s"
"It is a period of civil war. Rebel"
"spaceships, striking from a hidden"
"base, have won their first victory"
"against the evil Galactic Empire."
"\n" % json.dumps(notifications),
sys.stdout.getvalue())
@mock.patch("sys.stdout", six.StringIO())
@mock.patch("osprofiler.parsers.ceilometer.get_notifications")
@mock.patch("osprofiler.parsers.ceilometer.parse_notifications")
def test_trace_show_write_to_file(self, mock_notifications, mock_get):
mock_get.return_value = "some notification"
notifications = {
'info': {
'started': 0, 'finished': 0, 'name': 'total'}, 'children': []}
mock_notifications.return_value = notifications
with mock.patch("osprofiler.cmd.commands.open",
mock.mock_open(), create=True) as mock_open:
self.run_command("trace show fake_id --json --out='/file'")
output = mock_open.return_value.__enter__.return_value
output.write.assert_called_once_with(json.dumps(notifications))