1 ##############################################################################
2 # Copyright (c) 2017 Rajesh Kudaka <4k.rajesh@gmail.com>
3 # Copyright (c) 2018-2019 Intel Corporation.
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 ##############################################################################
11 """ Handler for yardstick command 'report' """
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
25 """Data structure to parse data for use with the JS library jsTree"""
27 self._created_nodes = ['#']
30 def _create_node(self, _id):
31 """Helper method for format_for_jstree to create each node.
33 Creates the node (and any required parents) and keeps track
36 :param _id: (string) id of the node to be created
39 components = _id.split(".")
41 if len(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)
51 self.jstree_data.append({"id": _id, "text": text, "parent": parent_id})
52 self._created_nodes.append(_id)
54 def format_for_jstree(self, data):
55 """Format the data into the required format for jsTree.
57 The data format expected is a list of metric names e.g.:
59 ['tg__0.DropPackets', 'tg__0.LatencyAvg.5']
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.::
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"},]
70 :param data: (list) data to be converted
73 self._created_nodes = ['#']
77 self._create_node(metric)
79 return self.jstree_data
85 Set of commands to manage reports.
93 def _validate(self, yaml_name, task_id):
94 if re.match(r"^[\w-]+$", yaml_name):
95 self.yaml_name = yaml_name
97 raise ValueError("invalid yaml_name", yaml_name)
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
104 raise ValueError("invalid task_id", task_id)
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)
113 raise KeyError("Test case not found.")
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)
122 raise KeyError("Task ID or Test case not found.")
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
132 def _get_timestamps(self, metrics, resolution=6):
133 # Extract the timestamps from a list of metrics
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
141 def _format_datasets(self, metric_name, metrics):
143 for metric in metrics:
144 val = metric.get(metric_name, None)
146 # keep explicit None or missing entry as is
148 elif isinstance(val, (int, float)):
149 # keep plain int or float as is
151 elif six.PY2 and isinstance(val,
152 long): # pylint: disable=undefined-variable
153 # PY2: long value would be rendered with trailing L,
154 # which JS does not support, so convert it to float
156 elif isinstance(val, six.string_types):
158 if not isinstance(s, str):
159 s = s.encode('utf8') # PY2: unicode to str
161 # convert until failure
165 if six.PY2 and isinstance(val,
166 long): # pylint: disable=undefined-variable
167 val = float(val) # PY2: long to float
169 # value may have been converted to a number
172 # if val was not converted into a num, then it must be
173 # text, which shouldn't end up in the report
174 if isinstance(val, six.string_types):
177 raise ValueError("Cannot convert %r" % val)
181 @cliargs("task_id", type=str, help=" task id", nargs=1)
182 @cliargs("yaml_name", type=str, help=" Yaml file Name", nargs=1)
183 def _generate_common(self, args):
184 """Actions that are common to both report formats.
186 Create the necessary data structure for rendering
187 the report templates.
189 self._validate(args.yaml_name[0], args.task_id[0])
191 db_fieldkeys = self._get_fieldkeys()
193 # - PY2: unicode key and unicode value
194 # - PY3: str key and str value
196 db_metrics = self._get_metrics()
198 # - PY2: unicode key and { None | unicode | float | long | int } value
199 # - PY3: str key and { None | str | float | int } value
201 # extract fieldKey entries, and convert them to str where needed
202 field_keys = [key if isinstance(key, str) # PY3: already str
203 else key.encode('utf8') # PY2: unicode to str
206 for field in db_fieldkeys]]
209 self.Timestamp = self._get_timestamps(db_metrics)
211 # prepare return values
213 table_vals = {'Timestamp': self.Timestamp}
215 # extract and convert field values
216 for key in field_keys:
217 values = self._format_datasets(key, db_metrics)
218 datasets.append({'label': key, 'data': values})
219 table_vals[key] = values
221 return datasets, table_vals
223 @cliargs("task_id", type=str, help=" task id", nargs=1)
224 @cliargs("yaml_name", type=str, help=" Yaml file Name", nargs=1)
225 def generate(self, args):
226 """Start report generation."""
227 datasets, table_vals = self._generate_common(args)
229 template_dir = consts.YARDSTICK_ROOT_PATH + "yardstick/common"
230 template_environment = jinja2.Environment(
232 loader=jinja2.FileSystemLoader(template_dir))
235 "datasets": datasets,
236 "Timestamps": self.Timestamp,
237 "task_id": self.task_id,
241 template_html = template_environment.get_template("report.html.j2")
243 with open(consts.DEFAULT_HTML_FILE, "w") as file_open:
244 file_open.write(template_html.render(context))
246 print("Report generated. View %s" % consts.DEFAULT_HTML_FILE)
248 @cliargs("task_id", type=str, help=" task id", nargs=1)
249 @cliargs("yaml_name", type=str, help=" Yaml file Name", nargs=1)
250 def generate_nsb(self, args):
251 """Start NSB report generation."""
252 _, report_data = self._generate_common(args)
253 report_time = report_data.pop('Timestamp')
254 report_keys = sorted(report_data, key=str.lower)
255 report_tree = JSTree().format_for_jstree(report_keys)
257 "testcase": self.yaml_name,
258 "task_id": self.task_id,
261 template_dir = consts.YARDSTICK_ROOT_PATH + "yardstick/common"
262 template_environment = jinja2.Environment(
264 loader=jinja2.FileSystemLoader(template_dir),
268 "report_meta": report_meta,
269 "report_data": report_data,
270 "report_time": report_time,
271 "report_keys": report_keys,
272 "report_tree": report_tree,
275 template_html = template_environment.get_template("nsb_report.html.j2")
277 with open(consts.DEFAULT_HTML_FILE, "w") as file_open:
278 file_open.write(template_html.render(context))
280 print("Report generated. View %s" % consts.DEFAULT_HTML_FILE)