blob: 5d6f67046bdf54069175fce0a89ec57b270b8204 [file] [log] [blame]
Brad Bishopa34c0302019-09-23 22:34:48 -04001#! /usr/bin/env python3
2#
3# Copyright (C) 2019 Garmin Ltd.
4#
5# SPDX-License-Identifier: GPL-2.0-only
6#
7
8import argparse
9import hashlib
10import logging
11import os
12import pprint
13import sys
14import threading
15import time
Andrew Geissler5199d832021-09-24 16:47:35 -050016import warnings
Patrick Williamsac13d5f2023-11-24 18:59:46 -060017import netrc
18import json
Andrew Geissleredff4922024-06-19 14:12:16 -040019import statistics
Andrew Geissler5199d832021-09-24 16:47:35 -050020warnings.simplefilter("default")
Brad Bishopa34c0302019-09-23 22:34:48 -040021
22try:
23 import tqdm
24 ProgressBar = tqdm.tqdm
25except ImportError:
26 class ProgressBar(object):
27 def __init__(self, *args, **kwargs):
28 pass
29
30 def __enter__(self):
31 return self
32
33 def __exit__(self, *args, **kwargs):
34 pass
35
36 def update(self):
37 pass
38
39sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), 'lib'))
40
41import hashserv
Patrick Williamsac13d5f2023-11-24 18:59:46 -060042import bb.asyncrpc
Brad Bishopa34c0302019-09-23 22:34:48 -040043
44DEFAULT_ADDRESS = 'unix://./hashserve.sock'
45METHOD = 'stress.test.method'
46
Patrick Williamsac13d5f2023-11-24 18:59:46 -060047def print_user(u):
48 print(f"Username: {u['username']}")
49 if "permissions" in u:
50 print("Permissions: " + " ".join(u["permissions"]))
51 if "token" in u:
52 print(f"Token: {u['token']}")
53
Brad Bishopa34c0302019-09-23 22:34:48 -040054
55def main():
Patrick Williamsda295312023-12-05 16:48:56 -060056 def handle_get(args, client):
57 result = client.get_taskhash(args.method, args.taskhash, all_properties=True)
58 if not result:
59 return 0
60
61 print(json.dumps(result, sort_keys=True, indent=4))
62 return 0
63
64 def handle_get_outhash(args, client):
65 result = client.get_outhash(args.method, args.outhash, args.taskhash)
66 if not result:
67 return 0
68
69 print(json.dumps(result, sort_keys=True, indent=4))
70 return 0
71
Brad Bishopa34c0302019-09-23 22:34:48 -040072 def handle_stats(args, client):
73 if args.reset:
74 s = client.reset_stats()
75 else:
76 s = client.get_stats()
Patrick Williamsac13d5f2023-11-24 18:59:46 -060077 print(json.dumps(s, sort_keys=True, indent=4))
Brad Bishopa34c0302019-09-23 22:34:48 -040078 return 0
79
80 def handle_stress(args, client):
81 def thread_main(pbar, lock):
82 nonlocal found_hashes
83 nonlocal missed_hashes
84 nonlocal max_time
Andrew Geissleredff4922024-06-19 14:12:16 -040085 nonlocal times
Brad Bishopa34c0302019-09-23 22:34:48 -040086
Patrick Williamsac13d5f2023-11-24 18:59:46 -060087 with hashserv.create_client(args.address) as client:
88 for i in range(args.requests):
89 taskhash = hashlib.sha256()
90 taskhash.update(args.taskhash_seed.encode('utf-8'))
91 taskhash.update(str(i).encode('utf-8'))
Brad Bishopa34c0302019-09-23 22:34:48 -040092
Patrick Williamsac13d5f2023-11-24 18:59:46 -060093 start_time = time.perf_counter()
94 l = client.get_unihash(METHOD, taskhash.hexdigest())
95 elapsed = time.perf_counter() - start_time
Brad Bishopa34c0302019-09-23 22:34:48 -040096
Patrick Williamsac13d5f2023-11-24 18:59:46 -060097 with lock:
98 if l:
99 found_hashes += 1
100 else:
101 missed_hashes += 1
Brad Bishopa34c0302019-09-23 22:34:48 -0400102
Andrew Geissleredff4922024-06-19 14:12:16 -0400103 times.append(elapsed)
Patrick Williamsac13d5f2023-11-24 18:59:46 -0600104 pbar.update()
Brad Bishopa34c0302019-09-23 22:34:48 -0400105
106 max_time = 0
107 found_hashes = 0
108 missed_hashes = 0
109 lock = threading.Lock()
Andrew Geissleredff4922024-06-19 14:12:16 -0400110 times = []
Brad Bishopa34c0302019-09-23 22:34:48 -0400111 start_time = time.perf_counter()
Andrew Geissleredff4922024-06-19 14:12:16 -0400112 with ProgressBar(total=args.clients * args.requests) as pbar:
Brad Bishopa34c0302019-09-23 22:34:48 -0400113 threads = [threading.Thread(target=thread_main, args=(pbar, lock), daemon=False) for _ in range(args.clients)]
114 for t in threads:
115 t.start()
116
117 for t in threads:
118 t.join()
Andrew Geissleredff4922024-06-19 14:12:16 -0400119 total_elapsed = time.perf_counter() - start_time
Brad Bishopa34c0302019-09-23 22:34:48 -0400120
Brad Bishopa34c0302019-09-23 22:34:48 -0400121 with lock:
Andrew Geissleredff4922024-06-19 14:12:16 -0400122 mean = statistics.mean(times)
123 median = statistics.median(times)
124 stddev = statistics.pstdev(times)
125
126 print(f"Number of clients: {args.clients}")
127 print(f"Requests per client: {args.requests}")
128 print(f"Number of requests: {len(times)}")
129 print(f"Total elapsed time: {total_elapsed:.3f}s")
130 print(f"Total request rate: {len(times)/total_elapsed:.3f} req/s")
131 print(f"Average request time: {mean:.3f}s")
132 print(f"Median request time: {median:.3f}s")
133 print(f"Request time std dev: {stddev:.3f}s")
134 print(f"Maximum request time: {max(times):.3f}s")
135 print(f"Minimum request time: {min(times):.3f}s")
136 print(f"Hashes found: {found_hashes}")
137 print(f"Hashes missed: {missed_hashes}")
Brad Bishopa34c0302019-09-23 22:34:48 -0400138
139 if args.report:
140 with ProgressBar(total=args.requests) as pbar:
141 for i in range(args.requests):
142 taskhash = hashlib.sha256()
143 taskhash.update(args.taskhash_seed.encode('utf-8'))
144 taskhash.update(str(i).encode('utf-8'))
145
146 outhash = hashlib.sha256()
147 outhash.update(args.outhash_seed.encode('utf-8'))
148 outhash.update(str(i).encode('utf-8'))
149
150 client.report_unihash(taskhash.hexdigest(), METHOD, outhash.hexdigest(), taskhash.hexdigest())
151
152 with lock:
153 pbar.update()
154
Andrew Geissler20137392023-10-12 04:59:14 -0600155 def handle_remove(args, client):
156 where = {k: v for k, v in args.where}
157 if where:
158 result = client.remove(where)
159 print("Removed %d row(s)" % (result["count"]))
160 else:
161 print("No query specified")
162
163 def handle_clean_unused(args, client):
164 result = client.clean_unused(args.max_age)
165 print("Removed %d rows" % (result["count"]))
166 return 0
167
Patrick Williamsac13d5f2023-11-24 18:59:46 -0600168 def handle_refresh_token(args, client):
169 r = client.refresh_token(args.username)
170 print_user(r)
171
172 def handle_set_user_permissions(args, client):
173 r = client.set_user_perms(args.username, args.permissions)
174 print_user(r)
175
176 def handle_get_user(args, client):
177 r = client.get_user(args.username)
178 print_user(r)
179
180 def handle_get_all_users(args, client):
181 users = client.get_all_users()
182 print("{username:20}| {permissions}".format(username="Username", permissions="Permissions"))
183 print(("-" * 20) + "+" + ("-" * 20))
184 for u in users:
185 print("{username:20}| {permissions}".format(username=u["username"], permissions=" ".join(u["permissions"])))
186
187 def handle_new_user(args, client):
188 r = client.new_user(args.username, args.permissions)
189 print_user(r)
190
191 def handle_delete_user(args, client):
192 r = client.delete_user(args.username)
193 print_user(r)
194
195 def handle_get_db_usage(args, client):
196 usage = client.get_db_usage()
197 print(usage)
198 tables = sorted(usage.keys())
199 print("{name:20}| {rows:20}".format(name="Table name", rows="Rows"))
200 print(("-" * 20) + "+" + ("-" * 20))
201 for t in tables:
202 print("{name:20}| {rows:<20}".format(name=t, rows=usage[t]["rows"]))
203 print()
204
205 total_rows = sum(t["rows"] for t in usage.values())
206 print(f"Total rows: {total_rows}")
207
208 def handle_get_db_query_columns(args, client):
209 columns = client.get_db_query_columns()
210 print("\n".join(sorted(columns)))
211
Patrick Williams73bd93f2024-02-20 08:07:48 -0600212 def handle_gc_status(args, client):
213 result = client.gc_status()
214 if not result["mark"]:
215 print("No Garbage collection in progress")
216 return 0
217
218 print("Current Mark: %s" % result["mark"])
219 print("Total hashes to keep: %d" % result["keep"])
220 print("Total hashes to remove: %s" % result["remove"])
221 return 0
222
223 def handle_gc_mark(args, client):
224 where = {k: v for k, v in args.where}
225 result = client.gc_mark(args.mark, where)
226 print("New hashes marked: %d" % result["count"])
227 return 0
228
229 def handle_gc_sweep(args, client):
230 result = client.gc_sweep(args.mark)
231 print("Removed %d rows" % result["count"])
232 return 0
233
234 def handle_unihash_exists(args, client):
235 result = client.unihash_exists(args.unihash)
236 if args.quiet:
237 return 0 if result else 1
238
239 print("true" if result else "false")
240 return 0
241
Andrew Geissleredff4922024-06-19 14:12:16 -0400242 def handle_ping(args, client):
243 times = []
244 for i in range(1, args.count + 1):
245 if not args.quiet:
246 print(f"Ping {i} of {args.count}... ", end="")
247 start_time = time.perf_counter()
248 client.ping()
249 elapsed = time.perf_counter() - start_time
250 times.append(elapsed)
251 if not args.quiet:
252 print(f"{elapsed:.3f}s")
253
254 mean = statistics.mean(times)
255 median = statistics.median(times)
256 std_dev = statistics.pstdev(times)
257
258 if not args.quiet:
259 print("------------------------")
260 print(f"Number of pings: {len(times)}")
261 print(f"Average round trip time: {mean:.3f}s")
262 print(f"Median round trip time: {median:.3f}s")
263 print(f"Round trip time std dev: {std_dev:.3f}s")
264 print(f"Min time is: {min(times):.3f}s")
265 print(f"Max time is: {max(times):.3f}s")
266 return 0
267
Brad Bishopa34c0302019-09-23 22:34:48 -0400268 parser = argparse.ArgumentParser(description='Hash Equivalence Client')
269 parser.add_argument('--address', default=DEFAULT_ADDRESS, help='Server address (default "%(default)s")')
270 parser.add_argument('--log', default='WARNING', help='Set logging level')
Patrick Williamsac13d5f2023-11-24 18:59:46 -0600271 parser.add_argument('--login', '-l', metavar="USERNAME", help="Authenticate as USERNAME")
272 parser.add_argument('--password', '-p', metavar="TOKEN", help="Authenticate using token TOKEN")
273 parser.add_argument('--become', '-b', metavar="USERNAME", help="Impersonate user USERNAME (if allowed) when performing actions")
274 parser.add_argument('--no-netrc', '-n', action="store_false", dest="netrc", help="Do not use .netrc")
Brad Bishopa34c0302019-09-23 22:34:48 -0400275
276 subparsers = parser.add_subparsers()
277
Patrick Williamsda295312023-12-05 16:48:56 -0600278 get_parser = subparsers.add_parser('get', help="Get the unihash for a taskhash")
279 get_parser.add_argument("method", help="Method to query")
280 get_parser.add_argument("taskhash", help="Task hash to query")
281 get_parser.set_defaults(func=handle_get)
282
283 get_outhash_parser = subparsers.add_parser('get-outhash', help="Get output hash information")
284 get_outhash_parser.add_argument("method", help="Method to query")
285 get_outhash_parser.add_argument("outhash", help="Output hash to query")
286 get_outhash_parser.add_argument("taskhash", help="Task hash to query")
287 get_outhash_parser.set_defaults(func=handle_get_outhash)
288
Brad Bishopa34c0302019-09-23 22:34:48 -0400289 stats_parser = subparsers.add_parser('stats', help='Show server stats')
290 stats_parser.add_argument('--reset', action='store_true',
291 help='Reset server stats')
292 stats_parser.set_defaults(func=handle_stats)
293
294 stress_parser = subparsers.add_parser('stress', help='Run stress test')
295 stress_parser.add_argument('--clients', type=int, default=10,
296 help='Number of simultaneous clients')
297 stress_parser.add_argument('--requests', type=int, default=1000,
298 help='Number of requests each client will perform')
299 stress_parser.add_argument('--report', action='store_true',
300 help='Report new hashes')
301 stress_parser.add_argument('--taskhash-seed', default='',
302 help='Include string in taskhash')
303 stress_parser.add_argument('--outhash-seed', default='',
304 help='Include string in outhash')
305 stress_parser.set_defaults(func=handle_stress)
306
Andrew Geissler20137392023-10-12 04:59:14 -0600307 remove_parser = subparsers.add_parser('remove', help="Remove hash entries")
308 remove_parser.add_argument("--where", "-w", metavar="KEY VALUE", nargs=2, action="append", default=[],
309 help="Remove entries from table where KEY == VALUE")
310 remove_parser.set_defaults(func=handle_remove)
311
312 clean_unused_parser = subparsers.add_parser('clean-unused', help="Remove unused database entries")
313 clean_unused_parser.add_argument("max_age", metavar="SECONDS", type=int, help="Remove unused entries older than SECONDS old")
314 clean_unused_parser.set_defaults(func=handle_clean_unused)
315
Patrick Williamsac13d5f2023-11-24 18:59:46 -0600316 refresh_token_parser = subparsers.add_parser('refresh-token', help="Refresh auth token")
317 refresh_token_parser.add_argument("--username", "-u", help="Refresh the token for another user (if authorized)")
318 refresh_token_parser.set_defaults(func=handle_refresh_token)
319
320 set_user_perms_parser = subparsers.add_parser('set-user-perms', help="Set new permissions for user")
321 set_user_perms_parser.add_argument("--username", "-u", help="Username", required=True)
322 set_user_perms_parser.add_argument("permissions", metavar="PERM", nargs="*", default=[], help="New permissions")
323 set_user_perms_parser.set_defaults(func=handle_set_user_permissions)
324
325 get_user_parser = subparsers.add_parser('get-user', help="Get user")
326 get_user_parser.add_argument("--username", "-u", help="Username")
327 get_user_parser.set_defaults(func=handle_get_user)
328
329 get_all_users_parser = subparsers.add_parser('get-all-users', help="List all users")
330 get_all_users_parser.set_defaults(func=handle_get_all_users)
331
332 new_user_parser = subparsers.add_parser('new-user', help="Create new user")
333 new_user_parser.add_argument("--username", "-u", help="Username", required=True)
334 new_user_parser.add_argument("permissions", metavar="PERM", nargs="*", default=[], help="New permissions")
335 new_user_parser.set_defaults(func=handle_new_user)
336
337 delete_user_parser = subparsers.add_parser('delete-user', help="Delete user")
338 delete_user_parser.add_argument("--username", "-u", help="Username", required=True)
339 delete_user_parser.set_defaults(func=handle_delete_user)
340
341 db_usage_parser = subparsers.add_parser('get-db-usage', help="Database Usage")
342 db_usage_parser.set_defaults(func=handle_get_db_usage)
343
344 db_query_columns_parser = subparsers.add_parser('get-db-query-columns', help="Show columns that can be used in database queries")
345 db_query_columns_parser.set_defaults(func=handle_get_db_query_columns)
346
Patrick Williams73bd93f2024-02-20 08:07:48 -0600347 gc_status_parser = subparsers.add_parser("gc-status", help="Show garbage collection status")
348 gc_status_parser.set_defaults(func=handle_gc_status)
349
350 gc_mark_parser = subparsers.add_parser('gc-mark', help="Mark hashes to be kept for garbage collection")
351 gc_mark_parser.add_argument("mark", help="Mark for this garbage collection operation")
352 gc_mark_parser.add_argument("--where", "-w", metavar="KEY VALUE", nargs=2, action="append", default=[],
353 help="Keep entries in table where KEY == VALUE")
354 gc_mark_parser.set_defaults(func=handle_gc_mark)
355
356 gc_sweep_parser = subparsers.add_parser('gc-sweep', help="Perform garbage collection and delete any entries that are not marked")
357 gc_sweep_parser.add_argument("mark", help="Mark for this garbage collection operation")
358 gc_sweep_parser.set_defaults(func=handle_gc_sweep)
359
360 unihash_exists_parser = subparsers.add_parser('unihash-exists', help="Check if a unihash is known to the server")
361 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")
362 unihash_exists_parser.add_argument("unihash", help="Unihash to check")
363 unihash_exists_parser.set_defaults(func=handle_unihash_exists)
364
Andrew Geissleredff4922024-06-19 14:12:16 -0400365 ping_parser = subparsers.add_parser('ping', help="Ping server")
366 ping_parser.add_argument("-n", "--count", type=int, help="Number of pings. Default is %(default)s", default=10)
367 ping_parser.add_argument("-q", "--quiet", action="store_true", help="Don't print each ping; only print results")
368 ping_parser.set_defaults(func=handle_ping)
369
Brad Bishopa34c0302019-09-23 22:34:48 -0400370 args = parser.parse_args()
371
372 logger = logging.getLogger('hashserv')
373
374 level = getattr(logging, args.log.upper(), None)
375 if not isinstance(level, int):
376 raise ValueError('Invalid log level: %s' % args.log)
377
378 logger.setLevel(level)
379 console = logging.StreamHandler()
380 console.setLevel(level)
381 logger.addHandler(console)
382
Patrick Williamsac13d5f2023-11-24 18:59:46 -0600383 login = args.login
384 password = args.password
385
386 if login is None and args.netrc:
387 try:
388 n = netrc.netrc()
389 auth = n.authenticators(args.address)
390 if auth is not None:
391 login, _, password = auth
392 except FileNotFoundError:
393 pass
Patrick Williams03514f12024-04-05 07:04:11 -0500394 except netrc.NetrcParseError as e:
395 sys.stderr.write(f"Error parsing {e.filename}:{e.lineno}: {e.msg}\n")
Patrick Williamsac13d5f2023-11-24 18:59:46 -0600396
Brad Bishopa34c0302019-09-23 22:34:48 -0400397 func = getattr(args, 'func', None)
398 if func:
Patrick Williamsac13d5f2023-11-24 18:59:46 -0600399 try:
400 with hashserv.create_client(args.address, login, password) as client:
401 if args.become:
402 client.become_user(args.become)
403 return func(args, client)
404 except bb.asyncrpc.InvokeError as e:
405 print(f"ERROR: {e}")
406 return 1
Brad Bishopa34c0302019-09-23 22:34:48 -0400407
408 return 0
409
410
411if __name__ == '__main__':
412 try:
413 ret = main()
414 except Exception:
415 ret = 1
416 import traceback
417 traceback.print_exc()
418 sys.exit(ret)