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_task_start_time(self):
125 # The start time should come from the task or the metadata table.
126 # The first entry into influx for a task will be AFTER the first TC
128 cmd = "select * from \"%s\" where task_id='%s' ORDER BY time ASC limit 1"
129 task_query = cmd % (self.yaml_name, self.task_id)
131 query_exec = influx.query(task_query)
132 start_time = query_exec[0]['time']
135 def _get_task_end_time(self):
136 # NOTE(elfoley): when using select first() and select last() for the
137 # DB query, the timestamp returned is 0, so later queries try to
138 # return metrics from 1970
139 cmd = "select * from \"%s\" where task_id='%s' ORDER BY time DESC limit 1"
140 task_query = cmd % (self.yaml_name, self.task_id)
141 query_exec = influx.query(task_query)
142 end_time = query_exec[0]['time']
145 def _get_baro_metrics(self):
146 start_time = self._get_task_start_time()
147 end_time = self._get_task_end_time()
149 "cpu_value", "cpufreq_value", "intel_pmu_value",
150 "virt_value", "memory_value"]
154 for metric in metric_list:
155 cmd = "select * from \"%s\" where time >= '%s' and time <= '%s'"
156 query = cmd % (metric, start_time, end_time)
157 query_exec[metric] = influx.query(query, db='collectd')
158 print("query_exec: {}".format(query_exec))
160 for metric in query_exec:
161 print("metric in query_exec: {}".format(metric))
162 met_values = query_exec[metric]
163 print("met_values: {}".format(met_values))
166 metric_name = str('.'.join(
168 'host', 'name', 'type', 'type_instance', 'instance'
171 if not metrics.get(metric_name):
172 metrics[metric_name] = {}
173 metric_time = self._get_trimmed_timestamp(x['time'])
174 times.append(metric_time)
176 metrics[metric_name][time] = x['value']
178 times = sorted(list(set(times)))
180 metrics['Timestamp'] = times
181 print("metrics: {}".format(metrics))
184 def _get_trimmed_timestamp(self, metric_time, resolution=4):
185 if not isinstance(metric_time, str):
186 metric_time = metric_time.encode('utf8') # PY2: unicode to str
187 metric_time = metric_time[11:] # skip date, keep time
188 head, _, tail = metric_time.partition('.') # split HH:MM:SS & nsZ
189 metric_time = head + '.' + tail[:resolution] # join HH:MM:SS & .us
192 def _get_timestamps(self, metrics, resolution=6):
193 # Extract the timestamps from a list of metrics
195 for metric in metrics:
196 metric_time = self._get_trimmed_timestamp(
197 metric['time'], resolution)
198 timestamps.append(metric_time) # HH:MM:SS.micros
201 def _format_datasets(self, metric_name, metrics):
203 for metric in metrics:
204 val = metric.get(metric_name, None)
206 # keep explicit None or missing entry as is
208 elif isinstance(val, (int, float)):
209 # keep plain int or float as is
211 elif six.PY2 and isinstance(val,
212 long): # pylint: disable=undefined-variable
213 # PY2: long value would be rendered with trailing L,
214 # which JS does not support, so convert it to float
216 elif isinstance(val, six.string_types):
218 if not isinstance(s, str):
219 s = s.encode('utf8') # PY2: unicode to str
221 # convert until failure
225 if six.PY2 and isinstance(val,
226 long): # pylint: disable=undefined-variable
227 val = float(val) # PY2: long to float
229 # value may have been converted to a number
232 # if val was not converted into a num, then it must be
233 # text, which shouldn't end up in the report
234 if isinstance(val, six.string_types):
237 raise ValueError("Cannot convert %r" % val)
241 @cliargs("task_id", type=str, help=" task id", nargs=1)
242 @cliargs("yaml_name", type=str, help=" Yaml file Name", nargs=1)
243 def _generate_common(self, args):
244 """Actions that are common to both report formats.
246 Create the necessary data structure for rendering
247 the report templates.
249 self._validate(args.yaml_name[0], args.task_id[0])
251 db_fieldkeys = self._get_fieldkeys()
253 # - PY2: unicode key and unicode value
254 # - PY3: str key and str value
256 db_metrics = self._get_metrics()
258 # - PY2: unicode key and { None | unicode | float | long | int } value
259 # - PY3: str key and { None | str | float | int } value
261 # extract fieldKey entries, and convert them to str where needed
262 field_keys = [key if isinstance(key, str) # PY3: already str
263 else key.encode('utf8') # PY2: unicode to str
266 for field in db_fieldkeys]]
269 self.Timestamp = self._get_timestamps(db_metrics)
271 # prepare return values
273 table_vals = {'Timestamp': self.Timestamp}
275 # extract and convert field values
276 for key in field_keys:
277 values = self._format_datasets(key, db_metrics)
278 datasets.append({'label': key, 'data': values})
279 table_vals[key] = values
281 return datasets, table_vals
283 @cliargs("task_id", type=str, help=" task id", nargs=1)
284 @cliargs("yaml_name", type=str, help=" Yaml file Name", nargs=1)
285 def generate(self, args):
286 """Start report generation."""
287 datasets, table_vals = self._generate_common(args)
289 template_dir = consts.YARDSTICK_ROOT_PATH + "yardstick/common"
290 template_environment = jinja2.Environment(
292 loader=jinja2.FileSystemLoader(template_dir))
295 "datasets": datasets,
296 "Timestamps": self.Timestamp,
297 "task_id": self.task_id,
301 template_html = template_environment.get_template("report.html.j2")
303 with open(consts.DEFAULT_HTML_FILE, "w") as file_open:
304 file_open.write(template_html.render(context))
306 print("Report generated. View %s" % consts.DEFAULT_HTML_FILE)
308 def _combine_times(self, *args):
310 # Combines an arbitrary number of lists
311 [times.extend(x) for x in args]
312 times = list(set(times))
316 def _combine_metrics(self, *args):
317 baro_data, baro_time, yard_data, yard_time = args
318 combo_time = self._combine_times(baro_time, yard_time)
321 [data.update(x) for x in (baro_data, yard_data)]
324 table_data['Timestamp'] = combo_time
326 keys = sorted(data.keys())
327 for met_name in data:
329 for point in data[met_name]:
330 dataset.append({'x': point, 'y': data[met_name][point]})
331 # the metrics need to be ordered by time
332 combo[met_name] = sorted(dataset, key=lambda i: i['x'])
333 for met_name in data:
334 table_data[met_name] = []
336 table_data[met_name].append(data[met_name].get(t, ''))
337 return combo, keys, table_data
339 @cliargs("task_id", type=str, help=" task id", nargs=1)
340 @cliargs("yaml_name", type=str, help=" Yaml file Name", nargs=1)
341 def generate_nsb(self, args):
342 """Start NSB report generation."""
343 _, report_data = self._generate_common(args)
344 report_time = report_data.pop('Timestamp')
345 report_keys = sorted(report_data, key=str.lower)
346 report_tree = JSTree().format_for_jstree(report_keys)
348 "testcase": self.yaml_name,
349 "task_id": self.task_id,
352 template_dir = consts.YARDSTICK_ROOT_PATH + "yardstick/common"
353 template_environment = jinja2.Environment(
355 loader=jinja2.FileSystemLoader(template_dir),
359 "report_meta": report_meta,
360 "report_data": report_data,
361 "report_time": report_time,
362 "report_keys": report_keys,
363 "report_tree": report_tree,
366 template_html = template_environment.get_template("nsb_report.html.j2")
368 with open(consts.DEFAULT_HTML_FILE, "w") as file_open:
369 file_open.write(template_html.render(context))
371 print("Report generated. View %s" % consts.DEFAULT_HTML_FILE)