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:
parent
b2c9b86ad7
commit
42ec4ccda1
@ -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
|
||||
|
0
osprofiler/cmd/__init__.py
Normal file
0
osprofiler/cmd/__init__.py
Normal file
57
osprofiler/cmd/cliutils.py
Normal file
57
osprofiler/cmd/cliutils.py
Normal 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))
|
84
osprofiler/cmd/commands.py
Normal file
84
osprofiler/cmd/commands.py
Normal 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
24
osprofiler/cmd/exc.py
Normal 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
243
osprofiler/cmd/shell.py
Normal 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()
|
209
osprofiler/cmd/template.html
Normal file
209
osprofiler/cmd/template.html
Normal 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>
|
@ -1 +1,3 @@
|
||||
argparse
|
||||
six>=1.7.0
|
||||
WebOb>=1.2.3
|
||||
|
@ -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
0
tests/cmd/__init__.py
Normal file
226
tests/cmd/test_shell.py
Normal file
226
tests/cmd/test_shell.py
Normal 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))
|
Loading…
x
Reference in New Issue
Block a user