blob: 1bf3a4aef9eb01daaba4892f0cd099a4281f6602 [file] [log] [blame]
David Riley9c382782017-11-23 16:41:34 -08001#!/usr/bin/python
2
3# Copyright 2017 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Load generator for devserver."""
8
David Rileyf0897b92017-12-14 13:25:35 -08009import argparse
David Riley9c382782017-11-23 16:41:34 -080010import ast
11import itertools
12import json
13import operator
14import pprint
15import re
16import sys
17
18import common
19from chromite.lib import commandline
20from chromite.lib import cros_logging as logging
21
22
23# Map ast to operator.
24OPERATORS = {
25 ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
26 ast.Div: operator.truediv, ast.USub: operator.neg,
27 ast.Not: operator.not_,
28 ast.Eq: operator.eq, ast.NotEq: operator.ne,
29 ast.Lt: operator.lt, ast.Gt: operator.gt,
30 ast.LtE: operator.le, ast.GtE: operator.ge,
31}
32
33# Default keys to skip displaying.
34DEFAULT_SKIP = [
35 'build_name',
36 'devserver',
37 'name',
38 'parent',
39 'quick_provision',
David Riley7a2a7b92017-12-06 15:24:20 -080040 'trigger_response',
David Riley9c382782017-11-23 16:41:34 -080041]
42
43# List of commandline arguments for easy filtering.
44FILTER_ARGS = [
45 'board',
46 'build_name',
47 'devserver',
48 'name',
49 'status',
50]
51
52
53def get_parser():
54 """Creates the argparse parser."""
55 parser = commandline.ArgumentParser(description=__doc__)
David Rileyf0897b92017-12-14 13:25:35 -080056 parser.add_argument('infile', nargs='*', type=argparse.FileType('r'),
57 help='Path to JSON file to read.',
58 default=[sys.stdin])
David Riley9c382782017-11-23 16:41:34 -080059 parser.add_argument('--boards', type=str, action='store',
60 help='Boards to show.')
61 parser.add_argument('--group', type=str, action='store',
62 help='Comma-spearated list of keys to group by.')
63 parser.add_argument('--dump', action='store_true',
64 help='Dump all filtered entries.')
65 parser.add_argument('--skip', type=str, action='store',
66 help='Comma-separated list of keys to skip displaying.',
67 default=','.join(DEFAULT_SKIP))
68 parser.add_argument('--filter', type=str, action='store',
69 help='Filter expression to apply to each node.')
70 for arg in FILTER_ARGS:
71 parser.add_argument('--%s' % arg, type=str, action='store',
72 help='Comma-separated list of %s to filter by.' % arg)
73
74 return parser
75
76def eval_entry(expr, entry):
77 """Perform evaluation of an expression.
78
79 Named variables are interpreted as key-values from entry.
80 """
81 return eval_node(ast.parse(expr, mode='eval').body, entry)
82
83def eval_node(node, entry):
84 """Perform evaluation of a node."""
85 if isinstance(node, ast.Num):
86 return node.n
87 elif isinstance(node, ast.Str):
88 return node.s
89 elif isinstance(node, ast.Name):
90 if node.id == 'True':
91 return True
92 elif node.id == 'False':
93 return False
94 else:
95 return entry[node.id]
96 elif isinstance(node, ast.BinOp):
97 return OPERATORS[type(node.op)](eval_node(node.left, entry),
98 eval_node(node.right, entry))
99 elif isinstance(node, ast.UnaryOp): # <operator> <operand> e.g., -1
100 return OPERATORS[type(node.op)](eval_node(node.operand, entry))
101 elif isinstance(node, ast.BoolOp): # <operator> <operand> e.g., -1
102 if isinstance(node.op, ast.And):
103 for value in node.values:
104 if not eval_node(value, entry):
105 return False
106 return True
107 elif isinstance(node.op, ast.Or):
108 for value in node.values:
109 if eval_node(value, entry):
110 return True
111 return False
112 else:
113 raise TypeError(node)
114 elif isinstance(node, ast.Compare): # <operator> <operand> e.g., -1
115 left = node.left
116 for op, comparator in zip(node.ops, node.comparators):
117 if not OPERATORS[type(op)](eval_node(left, entry),
118 eval_node(comparator, entry)):
119 return False
120 left = comparator
121 return True
122 elif isinstance(node, ast.Call):
123 if isinstance(node.func, ast.Name) and node.func.id == 'match':
124 return re.match(eval_node(node.args[0], entry),
125 eval_node(node.args[1], entry))
126 elif isinstance(node.func, ast.Name) and node.func.id == 'search':
127 return re.search(eval_node(node.args[0], entry),
128 eval_node(node.args[1], entry))
129 else:
130 raise TypeError(node)
131 else:
132 raise TypeError(node)
133
134def summarize_entries(entries, skip=set()):
135 """Summarize a list of entries."""
136 TAG_KEYS = [
137 'board', 'build_name', 'devserver', 'name',
138 'parent', 'quick_provision', 'status'
139 ]
140 VALUE_KEYS = [
141 'avg_active', 'elapsed',
142 ]
143 summary = {
144 'COUNT': len(entries),
145 }
146 summary.update({key: summarize_tags(entries, key) for key in TAG_KEYS
147 if key not in skip})
148 summary.update({key: summarize_values(entries, key) for key in VALUE_KEYS
149 if key not in skip})
150 return summary
151
152def summarize_tags(entries, key):
153 """Summarize all the different string values for a given key."""
154 tags = {str(entry[key]) for entry in entries}
155 return list(tags)
156
157def summarize_values(entries, key):
158 """Summarize the numeric values for a given key."""
159 if entries is None or len(entries) == 0:
160 return None
161
162 values = [entry[key] for entry in entries if key in entry]
163 summary = {}
164 num_values = len(values)
165 if num_values:
166 summary['min'] = min(values)
167 summary['max'] = max(values)
168 summary['avg'] = sum(values) / num_values
169 num_skipped = len(entries) - num_values
170 if num_skipped:
171 summary['num'] = num_values
172 summary['skipped'] = num_skipped
173 return summary
174
175def group_entries(keys, entries):
176 """Group entries based on different values of given keys.
177
178 @param keys: A list of keys to group by.
179 @param entries: A list of entries to split into groups.
180
181 @return A list of list of entries, where each list has a different key value.
182 """
183 if not keys:
184 return [entries]
185
186 # Divide the group based on the first key.
187 indexed = {}
188 for entry in entries:
189 value = str(entry[keys[0]])
190 indexed.setdefault(value, []).append(entry)
191 groups = [indexed[value] for value in sorted(indexed.keys())]
192
193 # Recursively subdivide all the groups based on the rest of the keys.
194 subgroups = []
195 for group in groups:
196 subgroups.extend(group_entries(keys[1:], group))
197 return subgroups
198
199def main(argv):
200 """Load generator for a devserver."""
201 parser = get_parser()
202 options = parser.parse_args(argv)
203
204 # Read entries from the specified file.
David Rileyf0897b92017-12-14 13:25:35 -0800205 all_entries = []
206 for f in options.infile:
207 all_entries.extend([json.loads(line) for line in f])
David Riley9c382782017-11-23 16:41:34 -0800208
209 # Filter entries:
210 # - Ignore non-provisions.
211 # - Filter via the specified FILTER_ARGS arguments.
212 # - Filter via explicit filter request.
213 entries = filter(lambda x: x['name'] != 'Runner', all_entries)
214 for arg in FILTER_ARGS:
215 if options.__dict__.get(arg):
216 entries = filter(lambda x: x[arg] in options.__dict__[arg].split(','),
217 entries)
218 if options.filter:
219 entries = filter(lambda x: eval_entry(options.filter, x), entries)
220
221 # Group the entries based on specified keys.
222 groups = group_entries(options.group.split(',') if options.group else None,
223 entries)
224
225 # Dump all filtered entries as groups, including their parents.
226 if options.dump:
227 dump_entries = itertools.chain(*groups)
228 # Dump all entries, tracking needed parents.
229 parents = []
230 for entry in dump_entries:
231 print(json.dumps(entry))
232 if entry['parent'] not in parents:
233 parents.append(entry['parent'])
234 # Dump all parents.
235 for entry in all_entries:
236 if entry['id'] in parents:
237 print(json.dumps(entry))
238
239 # Summarize the entries, group by group.
240 skip = options.skip.split(',') if options.skip else set()
241 summaries = [summarize_entries(group, skip) for group in groups]
242 print(json.dumps(summaries, indent=2))
243
244if __name__ == '__main__':
245 sys.exit(main(sys.argv[1:]))