diff options
Diffstat (limited to 'bitbake/bin/bitbake-hashclient')
-rwxr-xr-x | bitbake/bin/bitbake-hashclient | 351 |
1 files changed, 323 insertions, 28 deletions
diff --git a/bitbake/bin/bitbake-hashclient b/bitbake/bin/bitbake-hashclient index a89290217b..b8755c5797 100755 --- a/bitbake/bin/bitbake-hashclient +++ b/bitbake/bin/bitbake-hashclient | |||
@@ -13,6 +13,12 @@ import pprint | |||
13 | import sys | 13 | import sys |
14 | import threading | 14 | import threading |
15 | import time | 15 | import time |
16 | import warnings | ||
17 | import netrc | ||
18 | import json | ||
19 | import statistics | ||
20 | import textwrap | ||
21 | warnings.simplefilter("default") | ||
16 | 22 | ||
17 | try: | 23 | try: |
18 | import tqdm | 24 | import tqdm |
@@ -34,18 +40,42 @@ except ImportError: | |||
34 | sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), 'lib')) | 40 | sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), 'lib')) |
35 | 41 | ||
36 | import hashserv | 42 | import hashserv |
43 | import bb.asyncrpc | ||
37 | 44 | ||
38 | DEFAULT_ADDRESS = 'unix://./hashserve.sock' | 45 | DEFAULT_ADDRESS = 'unix://./hashserve.sock' |
39 | METHOD = 'stress.test.method' | 46 | METHOD = 'stress.test.method' |
40 | 47 | ||
48 | def print_user(u): | ||
49 | print(f"Username: {u['username']}") | ||
50 | if "permissions" in u: | ||
51 | print("Permissions: " + " ".join(u["permissions"])) | ||
52 | if "token" in u: | ||
53 | print(f"Token: {u['token']}") | ||
54 | |||
41 | 55 | ||
42 | def main(): | 56 | def main(): |
57 | def handle_get(args, client): | ||
58 | result = client.get_taskhash(args.method, args.taskhash, all_properties=True) | ||
59 | if not result: | ||
60 | return 0 | ||
61 | |||
62 | print(json.dumps(result, sort_keys=True, indent=4)) | ||
63 | return 0 | ||
64 | |||
65 | def handle_get_outhash(args, client): | ||
66 | result = client.get_outhash(args.method, args.outhash, args.taskhash) | ||
67 | if not result: | ||
68 | return 0 | ||
69 | |||
70 | print(json.dumps(result, sort_keys=True, indent=4)) | ||
71 | return 0 | ||
72 | |||
43 | def handle_stats(args, client): | 73 | def handle_stats(args, client): |
44 | if args.reset: | 74 | if args.reset: |
45 | s = client.reset_stats() | 75 | s = client.reset_stats() |
46 | else: | 76 | else: |
47 | s = client.get_stats() | 77 | s = client.get_stats() |
48 | pprint.pprint(s) | 78 | print(json.dumps(s, sort_keys=True, indent=4)) |
49 | return 0 | 79 | return 0 |
50 | 80 | ||
51 | def handle_stress(args, client): | 81 | def handle_stress(args, client): |
@@ -53,47 +83,59 @@ def main(): | |||
53 | nonlocal found_hashes | 83 | nonlocal found_hashes |
54 | nonlocal missed_hashes | 84 | nonlocal missed_hashes |
55 | nonlocal max_time | 85 | nonlocal max_time |
86 | nonlocal times | ||
56 | 87 | ||
57 | client = hashserv.create_client(args.address) | 88 | with hashserv.create_client(args.address) as client: |
58 | 89 | for i in range(args.requests): | |
59 | for i in range(args.requests): | 90 | taskhash = hashlib.sha256() |
60 | taskhash = hashlib.sha256() | 91 | taskhash.update(args.taskhash_seed.encode('utf-8')) |
61 | taskhash.update(args.taskhash_seed.encode('utf-8')) | 92 | taskhash.update(str(i).encode('utf-8')) |
62 | taskhash.update(str(i).encode('utf-8')) | ||
63 | 93 | ||
64 | start_time = time.perf_counter() | 94 | start_time = time.perf_counter() |
65 | l = client.get_unihash(METHOD, taskhash.hexdigest()) | 95 | l = client.get_unihash(METHOD, taskhash.hexdigest()) |
66 | elapsed = time.perf_counter() - start_time | 96 | elapsed = time.perf_counter() - start_time |
67 | 97 | ||
68 | with lock: | 98 | with lock: |
69 | if l: | 99 | if l: |
70 | found_hashes += 1 | 100 | found_hashes += 1 |
71 | else: | 101 | else: |
72 | missed_hashes += 1 | 102 | missed_hashes += 1 |
73 | 103 | ||
74 | max_time = max(elapsed, max_time) | 104 | times.append(elapsed) |
75 | pbar.update() | 105 | pbar.update() |
76 | 106 | ||
77 | max_time = 0 | 107 | max_time = 0 |
78 | found_hashes = 0 | 108 | found_hashes = 0 |
79 | missed_hashes = 0 | 109 | missed_hashes = 0 |
80 | lock = threading.Lock() | 110 | lock = threading.Lock() |
81 | total_requests = args.clients * args.requests | 111 | times = [] |
82 | start_time = time.perf_counter() | 112 | start_time = time.perf_counter() |
83 | with ProgressBar(total=total_requests) as pbar: | 113 | with ProgressBar(total=args.clients * args.requests) as pbar: |
84 | threads = [threading.Thread(target=thread_main, args=(pbar, lock), daemon=False) for _ in range(args.clients)] | 114 | threads = [threading.Thread(target=thread_main, args=(pbar, lock), daemon=False) for _ in range(args.clients)] |
85 | for t in threads: | 115 | for t in threads: |
86 | t.start() | 116 | t.start() |
87 | 117 | ||
88 | for t in threads: | 118 | for t in threads: |
89 | t.join() | 119 | t.join() |
120 | total_elapsed = time.perf_counter() - start_time | ||
90 | 121 | ||
91 | elapsed = time.perf_counter() - start_time | ||
92 | with lock: | 122 | with lock: |
93 | print("%d requests in %.1fs. %.1f requests per second" % (total_requests, elapsed, total_requests / elapsed)) | 123 | mean = statistics.mean(times) |
94 | print("Average request time %.8fs" % (elapsed / total_requests)) | 124 | median = statistics.median(times) |
95 | print("Max request time was %.8fs" % max_time) | 125 | stddev = statistics.pstdev(times) |
96 | print("Found %d hashes, missed %d" % (found_hashes, missed_hashes)) | 126 | |
127 | print(f"Number of clients: {args.clients}") | ||
128 | print(f"Requests per client: {args.requests}") | ||
129 | print(f"Number of requests: {len(times)}") | ||
130 | print(f"Total elapsed time: {total_elapsed:.3f}s") | ||
131 | print(f"Total request rate: {len(times)/total_elapsed:.3f} req/s") | ||
132 | print(f"Average request time: {mean:.3f}s") | ||
133 | print(f"Median request time: {median:.3f}s") | ||
134 | print(f"Request time std dev: {stddev:.3f}s") | ||
135 | print(f"Maximum request time: {max(times):.3f}s") | ||
136 | print(f"Minimum request time: {min(times):.3f}s") | ||
137 | print(f"Hashes found: {found_hashes}") | ||
138 | print(f"Hashes missed: {missed_hashes}") | ||
97 | 139 | ||
98 | if args.report: | 140 | if args.report: |
99 | with ProgressBar(total=args.requests) as pbar: | 141 | with ProgressBar(total=args.requests) as pbar: |
@@ -111,12 +153,173 @@ def main(): | |||
111 | with lock: | 153 | with lock: |
112 | pbar.update() | 154 | pbar.update() |
113 | 155 | ||
114 | parser = argparse.ArgumentParser(description='Hash Equivalence Client') | 156 | def handle_remove(args, client): |
157 | where = {k: v for k, v in args.where} | ||
158 | if where: | ||
159 | result = client.remove(where) | ||
160 | print("Removed %d row(s)" % (result["count"])) | ||
161 | else: | ||
162 | print("No query specified") | ||
163 | |||
164 | def handle_clean_unused(args, client): | ||
165 | result = client.clean_unused(args.max_age) | ||
166 | print("Removed %d rows" % (result["count"])) | ||
167 | return 0 | ||
168 | |||
169 | def handle_refresh_token(args, client): | ||
170 | r = client.refresh_token(args.username) | ||
171 | print_user(r) | ||
172 | |||
173 | def handle_set_user_permissions(args, client): | ||
174 | r = client.set_user_perms(args.username, args.permissions) | ||
175 | print_user(r) | ||
176 | |||
177 | def handle_get_user(args, client): | ||
178 | r = client.get_user(args.username) | ||
179 | print_user(r) | ||
180 | |||
181 | def handle_get_all_users(args, client): | ||
182 | users = client.get_all_users() | ||
183 | print("{username:20}| {permissions}".format(username="Username", permissions="Permissions")) | ||
184 | print(("-" * 20) + "+" + ("-" * 20)) | ||
185 | for u in users: | ||
186 | print("{username:20}| {permissions}".format(username=u["username"], permissions=" ".join(u["permissions"]))) | ||
187 | |||
188 | def handle_new_user(args, client): | ||
189 | r = client.new_user(args.username, args.permissions) | ||
190 | print_user(r) | ||
191 | |||
192 | def handle_delete_user(args, client): | ||
193 | r = client.delete_user(args.username) | ||
194 | print_user(r) | ||
195 | |||
196 | def handle_get_db_usage(args, client): | ||
197 | usage = client.get_db_usage() | ||
198 | print(usage) | ||
199 | tables = sorted(usage.keys()) | ||
200 | print("{name:20}| {rows:20}".format(name="Table name", rows="Rows")) | ||
201 | print(("-" * 20) + "+" + ("-" * 20)) | ||
202 | for t in tables: | ||
203 | print("{name:20}| {rows:<20}".format(name=t, rows=usage[t]["rows"])) | ||
204 | print() | ||
205 | |||
206 | total_rows = sum(t["rows"] for t in usage.values()) | ||
207 | print(f"Total rows: {total_rows}") | ||
208 | |||
209 | def handle_get_db_query_columns(args, client): | ||
210 | columns = client.get_db_query_columns() | ||
211 | print("\n".join(sorted(columns))) | ||
212 | |||
213 | def handle_gc_status(args, client): | ||
214 | result = client.gc_status() | ||
215 | if not result["mark"]: | ||
216 | print("No Garbage collection in progress") | ||
217 | return 0 | ||
218 | |||
219 | print("Current Mark: %s" % result["mark"]) | ||
220 | print("Total hashes to keep: %d" % result["keep"]) | ||
221 | print("Total hashes to remove: %s" % result["remove"]) | ||
222 | return 0 | ||
223 | |||
224 | def handle_gc_mark(args, client): | ||
225 | where = {k: v for k, v in args.where} | ||
226 | result = client.gc_mark(args.mark, where) | ||
227 | print("New hashes marked: %d" % result["count"]) | ||
228 | return 0 | ||
229 | |||
230 | def handle_gc_mark_stream(args, client): | ||
231 | stdin = (l.strip() for l in sys.stdin) | ||
232 | marked_hashes = 0 | ||
233 | |||
234 | try: | ||
235 | result = client.gc_mark_stream(args.mark, stdin) | ||
236 | marked_hashes = result["count"] | ||
237 | except ConnectionError: | ||
238 | logger.warning( | ||
239 | "Server doesn't seem to support `gc-mark-stream`. Sending " | ||
240 | "hashes sequentially using `gc-mark` API." | ||
241 | ) | ||
242 | for line in stdin: | ||
243 | pairs = line.split() | ||
244 | condition = dict(zip(pairs[::2], pairs[1::2])) | ||
245 | result = client.gc_mark(args.mark, condition) | ||
246 | marked_hashes += result["count"] | ||
247 | |||
248 | print("New hashes marked: %d" % marked_hashes) | ||
249 | return 0 | ||
250 | |||
251 | def handle_gc_sweep(args, client): | ||
252 | result = client.gc_sweep(args.mark) | ||
253 | print("Removed %d rows" % result["count"]) | ||
254 | return 0 | ||
255 | |||
256 | def handle_unihash_exists(args, client): | ||
257 | result = client.unihash_exists(args.unihash) | ||
258 | if args.quiet: | ||
259 | return 0 if result else 1 | ||
260 | |||
261 | print("true" if result else "false") | ||
262 | return 0 | ||
263 | |||
264 | def handle_ping(args, client): | ||
265 | times = [] | ||
266 | for i in range(1, args.count + 1): | ||
267 | if not args.quiet: | ||
268 | print(f"Ping {i} of {args.count}... ", end="") | ||
269 | start_time = time.perf_counter() | ||
270 | client.ping() | ||
271 | elapsed = time.perf_counter() - start_time | ||
272 | times.append(elapsed) | ||
273 | if not args.quiet: | ||
274 | print(f"{elapsed:.3f}s") | ||
275 | |||
276 | mean = statistics.mean(times) | ||
277 | median = statistics.median(times) | ||
278 | std_dev = statistics.pstdev(times) | ||
279 | |||
280 | if not args.quiet: | ||
281 | print("------------------------") | ||
282 | print(f"Number of pings: {len(times)}") | ||
283 | print(f"Average round trip time: {mean:.3f}s") | ||
284 | print(f"Median round trip time: {median:.3f}s") | ||
285 | print(f"Round trip time std dev: {std_dev:.3f}s") | ||
286 | print(f"Min time is: {min(times):.3f}s") | ||
287 | print(f"Max time is: {max(times):.3f}s") | ||
288 | return 0 | ||
289 | |||
290 | parser = argparse.ArgumentParser( | ||
291 | formatter_class=argparse.RawDescriptionHelpFormatter, | ||
292 | description='Hash Equivalence Client', | ||
293 | epilog=textwrap.dedent( | ||
294 | """ | ||
295 | Possible ADDRESS options are: | ||
296 | unix://PATH Connect to UNIX domain socket at PATH | ||
297 | ws://HOST[:PORT] Connect to websocket at HOST:PORT (default port is 80) | ||
298 | wss://HOST[:PORT] Connect to secure websocket at HOST:PORT (default port is 443) | ||
299 | HOST:PORT Connect to TCP server at HOST:PORT | ||
300 | """ | ||
301 | ), | ||
302 | ) | ||
115 | parser.add_argument('--address', default=DEFAULT_ADDRESS, help='Server address (default "%(default)s")') | 303 | parser.add_argument('--address', default=DEFAULT_ADDRESS, help='Server address (default "%(default)s")') |
116 | parser.add_argument('--log', default='WARNING', help='Set logging level') | 304 | parser.add_argument('--log', default='WARNING', help='Set logging level') |
305 | parser.add_argument('--login', '-l', metavar="USERNAME", help="Authenticate as USERNAME") | ||
306 | parser.add_argument('--password', '-p', metavar="TOKEN", help="Authenticate using token TOKEN") | ||
307 | parser.add_argument('--become', '-b', metavar="USERNAME", help="Impersonate user USERNAME (if allowed) when performing actions") | ||
308 | parser.add_argument('--no-netrc', '-n', action="store_false", dest="netrc", help="Do not use .netrc") | ||
117 | 309 | ||
118 | subparsers = parser.add_subparsers() | 310 | subparsers = parser.add_subparsers() |
119 | 311 | ||
312 | get_parser = subparsers.add_parser('get', help="Get the unihash for a taskhash") | ||
313 | get_parser.add_argument("method", help="Method to query") | ||
314 | get_parser.add_argument("taskhash", help="Task hash to query") | ||
315 | get_parser.set_defaults(func=handle_get) | ||
316 | |||
317 | get_outhash_parser = subparsers.add_parser('get-outhash', help="Get output hash information") | ||
318 | get_outhash_parser.add_argument("method", help="Method to query") | ||
319 | get_outhash_parser.add_argument("outhash", help="Output hash to query") | ||
320 | get_outhash_parser.add_argument("taskhash", help="Task hash to query") | ||
321 | get_outhash_parser.set_defaults(func=handle_get_outhash) | ||
322 | |||
120 | stats_parser = subparsers.add_parser('stats', help='Show server stats') | 323 | stats_parser = subparsers.add_parser('stats', help='Show server stats') |
121 | stats_parser.add_argument('--reset', action='store_true', | 324 | stats_parser.add_argument('--reset', action='store_true', |
122 | help='Reset server stats') | 325 | help='Reset server stats') |
@@ -135,6 +338,79 @@ def main(): | |||
135 | help='Include string in outhash') | 338 | help='Include string in outhash') |
136 | stress_parser.set_defaults(func=handle_stress) | 339 | stress_parser.set_defaults(func=handle_stress) |
137 | 340 | ||
341 | remove_parser = subparsers.add_parser('remove', help="Remove hash entries") | ||
342 | remove_parser.add_argument("--where", "-w", metavar="KEY VALUE", nargs=2, action="append", default=[], | ||
343 | help="Remove entries from table where KEY == VALUE") | ||
344 | remove_parser.set_defaults(func=handle_remove) | ||
345 | |||
346 | clean_unused_parser = subparsers.add_parser('clean-unused', help="Remove unused database entries") | ||
347 | clean_unused_parser.add_argument("max_age", metavar="SECONDS", type=int, help="Remove unused entries older than SECONDS old") | ||
348 | clean_unused_parser.set_defaults(func=handle_clean_unused) | ||
349 | |||
350 | refresh_token_parser = subparsers.add_parser('refresh-token', help="Refresh auth token") | ||
351 | refresh_token_parser.add_argument("--username", "-u", help="Refresh the token for another user (if authorized)") | ||
352 | refresh_token_parser.set_defaults(func=handle_refresh_token) | ||
353 | |||
354 | set_user_perms_parser = subparsers.add_parser('set-user-perms', help="Set new permissions for user") | ||
355 | set_user_perms_parser.add_argument("--username", "-u", help="Username", required=True) | ||
356 | set_user_perms_parser.add_argument("permissions", metavar="PERM", nargs="*", default=[], help="New permissions") | ||
357 | set_user_perms_parser.set_defaults(func=handle_set_user_permissions) | ||
358 | |||
359 | get_user_parser = subparsers.add_parser('get-user', help="Get user") | ||
360 | get_user_parser.add_argument("--username", "-u", help="Username") | ||
361 | get_user_parser.set_defaults(func=handle_get_user) | ||
362 | |||
363 | get_all_users_parser = subparsers.add_parser('get-all-users', help="List all users") | ||
364 | get_all_users_parser.set_defaults(func=handle_get_all_users) | ||
365 | |||
366 | new_user_parser = subparsers.add_parser('new-user', help="Create new user") | ||
367 | new_user_parser.add_argument("--username", "-u", help="Username", required=True) | ||
368 | new_user_parser.add_argument("permissions", metavar="PERM", nargs="*", default=[], help="New permissions") | ||
369 | new_user_parser.set_defaults(func=handle_new_user) | ||
370 | |||
371 | delete_user_parser = subparsers.add_parser('delete-user', help="Delete user") | ||
372 | delete_user_parser.add_argument("--username", "-u", help="Username", required=True) | ||
373 | delete_user_parser.set_defaults(func=handle_delete_user) | ||
374 | |||
375 | db_usage_parser = subparsers.add_parser('get-db-usage', help="Database Usage") | ||
376 | db_usage_parser.set_defaults(func=handle_get_db_usage) | ||
377 | |||
378 | db_query_columns_parser = subparsers.add_parser('get-db-query-columns', help="Show columns that can be used in database queries") | ||
379 | db_query_columns_parser.set_defaults(func=handle_get_db_query_columns) | ||
380 | |||
381 | gc_status_parser = subparsers.add_parser("gc-status", help="Show garbage collection status") | ||
382 | gc_status_parser.set_defaults(func=handle_gc_status) | ||
383 | |||
384 | gc_mark_parser = subparsers.add_parser('gc-mark', help="Mark hashes to be kept for garbage collection") | ||
385 | gc_mark_parser.add_argument("mark", help="Mark for this garbage collection operation") | ||
386 | gc_mark_parser.add_argument("--where", "-w", metavar="KEY VALUE", nargs=2, action="append", default=[], | ||
387 | help="Keep entries in table where KEY == VALUE") | ||
388 | gc_mark_parser.set_defaults(func=handle_gc_mark) | ||
389 | |||
390 | gc_mark_parser_stream = subparsers.add_parser( | ||
391 | 'gc-mark-stream', | ||
392 | help=( | ||
393 | "Mark multiple hashes to be retained for garbage collection. Input should be provided via stdin, " | ||
394 | "with each line formatted as key-value pairs separated by spaces, for example 'column1 foo column2 bar'." | ||
395 | ) | ||
396 | ) | ||
397 | gc_mark_parser_stream.add_argument("mark", help="Mark for this garbage collection operation") | ||
398 | gc_mark_parser_stream.set_defaults(func=handle_gc_mark_stream) | ||
399 | |||
400 | gc_sweep_parser = subparsers.add_parser('gc-sweep', help="Perform garbage collection and delete any entries that are not marked") | ||
401 | gc_sweep_parser.add_argument("mark", help="Mark for this garbage collection operation") | ||
402 | gc_sweep_parser.set_defaults(func=handle_gc_sweep) | ||
403 | |||
404 | unihash_exists_parser = subparsers.add_parser('unihash-exists', help="Check if a unihash is known to the server") | ||
405 | unihash_exists_parser.add_argument("--quiet", action="store_true", help="Don't print status. Instead, exit with 0 if unihash exists and 1 if it does not") | ||
406 | unihash_exists_parser.add_argument("unihash", help="Unihash to check") | ||
407 | unihash_exists_parser.set_defaults(func=handle_unihash_exists) | ||
408 | |||
409 | ping_parser = subparsers.add_parser('ping', help="Ping server") | ||
410 | ping_parser.add_argument("-n", "--count", type=int, help="Number of pings. Default is %(default)s", default=10) | ||
411 | ping_parser.add_argument("-q", "--quiet", action="store_true", help="Don't print each ping; only print results") | ||
412 | ping_parser.set_defaults(func=handle_ping) | ||
413 | |||
138 | args = parser.parse_args() | 414 | args = parser.parse_args() |
139 | 415 | ||
140 | logger = logging.getLogger('hashserv') | 416 | logger = logging.getLogger('hashserv') |
@@ -148,11 +424,30 @@ def main(): | |||
148 | console.setLevel(level) | 424 | console.setLevel(level) |
149 | logger.addHandler(console) | 425 | logger.addHandler(console) |
150 | 426 | ||
427 | login = args.login | ||
428 | password = args.password | ||
429 | |||
430 | if login is None and args.netrc: | ||
431 | try: | ||
432 | n = netrc.netrc() | ||
433 | auth = n.authenticators(args.address) | ||
434 | if auth is not None: | ||
435 | login, _, password = auth | ||
436 | except FileNotFoundError: | ||
437 | pass | ||
438 | except netrc.NetrcParseError as e: | ||
439 | sys.stderr.write(f"Error parsing {e.filename}:{e.lineno}: {e.msg}\n") | ||
440 | |||
151 | func = getattr(args, 'func', None) | 441 | func = getattr(args, 'func', None) |
152 | if func: | 442 | if func: |
153 | client = hashserv.create_client(args.address) | 443 | try: |
154 | 444 | with hashserv.create_client(args.address, login, password) as client: | |
155 | return func(args, client) | 445 | if args.become: |
446 | client.become_user(args.become) | ||
447 | return func(args, client) | ||
448 | except bb.asyncrpc.InvokeError as e: | ||
449 | print(f"ERROR: {e}") | ||
450 | return 1 | ||
156 | 451 | ||
157 | return 0 | 452 | return 0 |
158 | 453 | ||