b7d2fd02bc6be7fd6d94fc381d0ac13d222045cc
[yardstick.git] / yardstick / benchmark / core / report.py
1 ##############################################################################
2 # Copyright (c) 2017 Rajesh Kudaka <4k.rajesh@gmail.com>
3 # Copyright (c) 2018-2019 Intel Corporation.
4 #
5 # All rights reserved. This program and the accompanying materials
6 # are made available under the terms of the Apache License, Version 2.0
7 # which accompanies this distribution, and is available at
8 # http://www.apache.org/licenses/LICENSE-2.0
9 ##############################################################################
10
11 """ Handler for yardstick command 'report' """
12
13 import re
14 import six
15 import uuid
16
17 import jinja2
18 from api.utils import influx
19 from oslo_utils import uuidutils
20 from yardstick.common import constants as consts
21 from yardstick.common.utils import cliargs
22
23
24 class JSTree(object):
25     """Data structure to parse data for use with the JS library jsTree"""
26     def __init__(self):
27         self._created_nodes = ['#']
28         self.jstree_data = []
29
30     def _create_node(self, _id):
31         """Helper method for format_for_jstree to create each node.
32
33         Creates the node (and any required parents) and keeps track
34         of the created nodes.
35
36         :param _id: (string) id of the node to be created
37         :return: None
38         """
39         components = _id.split(".")
40
41         if len(components) == 1:
42             text = components[0]
43             parent_id = "#"
44         else:
45             text = components[-1]
46             parent_id = ".".join(components[:-1])
47             # make sure the parent has been created
48             if not parent_id in self._created_nodes:
49                 self._create_node(parent_id)
50
51         self.jstree_data.append({"id": _id, "text": text, "parent": parent_id})
52         self._created_nodes.append(_id)
53
54     def format_for_jstree(self, data):
55         """Format the data into the required format for jsTree.
56
57         The data format expected is a list of metric names e.g.:
58
59             ['tg__0.DropPackets', 'tg__0.LatencyAvg.5']
60
61         This data is converted into the format required for jsTree to group and
62         display the metrics in a hierarchial fashion, including creating a
63         number of parent nodes e.g.::
64
65             [{"id": "tg__0", "text": "tg__0", "parent": "#"},
66              {"id": "tg__0.DropPackets", "text": "DropPackets", "parent": "tg__0"},
67              {"id": "tg__0.LatencyAvg", "text": "LatencyAvg", "parent": "tg__0"},
68              {"id": "tg__0.LatencyAvg.5", "text": "5", "parent": "tg__0.LatencyAvg"},]
69
70         :param data: (list) data to be converted
71         :return: list
72         """
73         self._created_nodes = ['#']
74         self.jstree_data = []
75
76         for metric in data:
77             self._create_node(metric)
78
79         return self.jstree_data
80
81
82 class Report(object):
83     """Report commands.
84
85     Set of commands to manage reports.
86     """
87
88     def __init__(self):
89         self.Timestamp = []
90         self.yaml_name = ""
91         self.task_id = ""
92
93     def _validate(self, yaml_name, task_id):
94         if re.match(r"^[\w-]+$", yaml_name):
95             self.yaml_name = yaml_name
96         else:
97             raise ValueError("invalid yaml_name", yaml_name)
98
99         if uuidutils.is_uuid_like(task_id):
100             task_id = '{' + task_id + '}'
101             task_uuid = (uuid.UUID(task_id))
102             self.task_id = task_uuid
103         else:
104             raise ValueError("invalid task_id", task_id)
105
106     def _get_fieldkeys(self):
107         fieldkeys_cmd = "show field keys from \"%s\""
108         fieldkeys_query = fieldkeys_cmd % (self.yaml_name)
109         query_exec = influx.query(fieldkeys_query)
110         if query_exec:
111             return query_exec
112         else:
113             raise KeyError("Test case not found.")
114
115     def _get_metrics(self):
116         metrics_cmd = "select * from \"%s\" where task_id = '%s'"
117         metrics_query = metrics_cmd % (self.yaml_name, self.task_id)
118         query_exec = influx.query(metrics_query)
119         if query_exec:
120             return query_exec
121         else:
122             raise KeyError("Task ID or Test case not found.")
123
124     def _get_trimmed_timestamp(self, metric_time, resolution=4):
125         if not isinstance(metric_time, str):
126             metric_time = metric_time.encode('utf8') # PY2: unicode to str
127         metric_time = metric_time[11:]               # skip date, keep time
128         head, _, tail = metric_time.partition('.')   # split HH:MM:SS & nsZ
129         metric_time = head + '.' + tail[:resolution] # join HH:MM:SS & .us
130         return metric_time
131
132     def _get_timestamps(self, metrics, resolution=6):
133         # Extract the timestamps from a list of metrics
134         timestamps = []
135         for metric in metrics:
136             metric_time = self._get_trimmed_timestamp(
137                 metric['time'], resolution)
138             timestamps.append(metric_time)               # HH:MM:SS.micros
139         return timestamps
140
141     @cliargs("task_id", type=str, help=" task id", nargs=1)
142     @cliargs("yaml_name", type=str, help=" Yaml file Name", nargs=1)
143     def _generate_common(self, args):
144         """Actions that are common to both report formats.
145
146         Create the necessary data structure for rendering
147         the report templates.
148         """
149         self._validate(args.yaml_name[0], args.task_id[0])
150
151         db_fieldkeys = self._get_fieldkeys()
152         # list of dicts of:
153         # - PY2: unicode key and unicode value
154         # - PY3: str key and str value
155
156         db_metrics = self._get_metrics()
157         # list of dicts of:
158         # - PY2: unicode key and { None | unicode | float | long | int } value
159         # - PY3: str key and { None | str | float | int } value
160
161         # extract fieldKey entries, and convert them to str where needed
162         field_keys = [key if isinstance(key, str)       # PY3: already str
163                           else key.encode('utf8')       # PY2: unicode to str
164                       for key in
165                           [field['fieldKey']
166                            for field in db_fieldkeys]]
167
168         # extract timestamps
169         self.Timestamp = self._get_timestamps(db_metrics)
170
171         # prepare return values
172         datasets = []
173         table_vals = {'Timestamp': self.Timestamp}
174
175         # extract and convert field values
176         for key in field_keys:
177             values = []
178             for metric in db_metrics:
179                 val = metric.get(key, None)
180                 if val is None:
181                     # keep explicit None or missing entry as is
182                     pass
183                 elif isinstance(val, (int, float)):
184                     # keep plain int or float as is
185                     pass
186                 elif six.PY2 and isinstance(val,
187                             long):  # pylint: disable=undefined-variable
188                     # PY2: long value would be rendered with trailing L,
189                     # which JS does not support, so convert it to float
190                     val = float(val)
191                 elif isinstance(val, six.string_types):
192                     s = val
193                     if not isinstance(s, str):
194                         s = s.encode('utf8')            # PY2: unicode to str
195                     try:
196                         # convert until failure
197                         val = s
198                         val = float(s)
199                         val = int(s)
200                         if six.PY2 and isinstance(val,
201                                     long):  # pylint: disable=undefined-variable
202                             val = float(val)            # PY2: long to float
203                     except ValueError:
204                         pass
205                 else:
206                     raise ValueError("Cannot convert %r" % val)
207                 values.append(val)
208             datasets.append({'label': key, 'data': values})
209             table_vals[key] = values
210
211         return datasets, table_vals
212
213     @cliargs("task_id", type=str, help=" task id", nargs=1)
214     @cliargs("yaml_name", type=str, help=" Yaml file Name", nargs=1)
215     def generate(self, args):
216         """Start report generation."""
217         datasets, table_vals = self._generate_common(args)
218
219         template_dir = consts.YARDSTICK_ROOT_PATH + "yardstick/common"
220         template_environment = jinja2.Environment(
221             autoescape=False,
222             loader=jinja2.FileSystemLoader(template_dir))
223
224         context = {
225             "datasets": datasets,
226             "Timestamps": self.Timestamp,
227             "task_id": self.task_id,
228             "table": table_vals,
229         }
230
231         template_html = template_environment.get_template("report.html.j2")
232
233         with open(consts.DEFAULT_HTML_FILE, "w") as file_open:
234             file_open.write(template_html.render(context))
235
236         print("Report generated. View %s" % consts.DEFAULT_HTML_FILE)
237
238     @cliargs("task_id", type=str, help=" task id", nargs=1)
239     @cliargs("yaml_name", type=str, help=" Yaml file Name", nargs=1)
240     def generate_nsb(self, args):
241         """Start NSB report generation."""
242         _, report_data = self._generate_common(args)
243         report_time = report_data.pop('Timestamp')
244         report_keys = sorted(report_data, key=str.lower)
245         report_tree = JSTree().format_for_jstree(report_keys)
246         report_meta = {
247             "testcase": self.yaml_name,
248             "task_id": self.task_id,
249         }
250
251         template_dir = consts.YARDSTICK_ROOT_PATH + "yardstick/common"
252         template_environment = jinja2.Environment(
253             autoescape=False,
254             loader=jinja2.FileSystemLoader(template_dir),
255             lstrip_blocks=True)
256
257         context = {
258             "report_meta": report_meta,
259             "report_data": report_data,
260             "report_time": report_time,
261             "report_keys": report_keys,
262             "report_tree": report_tree,
263         }
264
265         template_html = template_environment.get_template("nsb_report.html.j2")
266
267         with open(consts.DEFAULT_HTML_FILE, "w") as file_open:
268             file_open.write(template_html.render(context))
269
270         print("Report generated. View %s" % consts.DEFAULT_HTML_FILE)