From fd2589d3107b0dc2c2547297ccfa0e6a5771a75e Mon Sep 17 00:00:00 2001 From: Christopher Moussa Date: Mon, 7 Oct 2024 11:47:06 -0700 Subject: [PATCH] flux-account: add new list-projects cmd Problem: There currently exists no way to list all of the currently registered projects in the flux-accounting database. Add a new command to the flux-accounting command suite that creates a table of all of the currently registered projects in the flux-accounting database. Add a couple sharness tests to t1025-flux-account-projects.t that check the output of calling list-projects with just the default project listed and after registering a couple of other projects. --- .../accounting/project_subcommands.py | 29 +++++++++++++++++++ src/cmd/flux-account-service.py | 15 ++++++++++ src/cmd/flux-account.py | 16 ++++++++++ t/t1025-flux-account-projects.t | 22 ++++++++++++++ 4 files changed, 82 insertions(+) diff --git a/src/bindings/python/fluxacct/accounting/project_subcommands.py b/src/bindings/python/fluxacct/accounting/project_subcommands.py index 1c2d2ae1d..ff823815b 100644 --- a/src/bindings/python/fluxacct/accounting/project_subcommands.py +++ b/src/bindings/python/fluxacct/accounting/project_subcommands.py @@ -115,3 +115,32 @@ def delete_project(conn, project): return warning_stmt return 0 + + +def list_projects(conn): + """ + List all of the available projects registered in the project_table. + """ + cur = conn.cursor() + + cur.execute("SELECT * FROM project_table") + rows = cur.fetchall() + + # fetch column names and determine width of each column + col_names = [description[0] for description in cur.description] + col_widths = [ + max(len(str(value)) for value in [col] + [row[i] for row in rows]) + for i, col in enumerate(col_names) + ] + + def format_row(row): + return " | ".join( + [f"{str(value).ljust(col_widths[i])}" for i, value in enumerate(row)] + ) + + header = format_row(col_names) + separator = "-+-".join(["-" * width for width in col_widths]) + data_rows = "\n".join([format_row(row) for row in rows]) + table = f"{header}\n{separator}\n{data_rows}" + + return table diff --git a/src/cmd/flux-account-service.py b/src/cmd/flux-account-service.py index e448354ee..2b63b88b1 100755 --- a/src/cmd/flux-account-service.py +++ b/src/cmd/flux-account-service.py @@ -92,6 +92,7 @@ def __init__(self, flux_handle, conn): "view_job_records", "view_queue", "view_project", + "list_projects", ] privileged_endpoints = [ @@ -513,6 +514,20 @@ def delete_project(self, handle, watcher, msg, arg): msg, 0, f"a non-OSError exception was caught: {str(exc)}" ) + def list_projects(self, handle, watcher, msg, arg): + try: + val = p.list_projects(self.conn) + + payload = {"list_projects": val} + + handle.respond(msg, payload) + except KeyError as exc: + handle.respond_error(msg, 0, f"missing key in payload: {exc}") + except Exception as exc: + handle.respond_error( + msg, 0, f"a non-OSError exception was caught: {str(exc)}" + ) + def scrub_old_jobs(self, handle, watcher, msg, arg): try: val = jobs.scrub_old_jobs(self.conn, msg.payload["num_weeks"]) diff --git a/src/cmd/flux-account.py b/src/cmd/flux-account.py index 63a5ef9ac..ebc09f1dd 100755 --- a/src/cmd/flux-account.py +++ b/src/cmd/flux-account.py @@ -519,6 +519,16 @@ def add_delete_project_arg(subparsers): ) +def add_list_projects_arg(subparsers): + subparser_list_projects = subparsers.add_parser( + "list-projects", + help="list all registered projects", + formatter_class=flux.util.help_formatter(), + ) + + subparser_list_projects.set_defaults(func="list_projects") + + def add_scrub_job_records_arg(subparsers): subparser = subparsers.add_parser( "scrub-old-jobs", @@ -628,6 +638,7 @@ def add_arguments_to_parser(parser, subparsers): add_add_project_arg(subparsers) add_view_project_arg(subparsers) add_delete_project_arg(subparsers) + add_list_projects_arg(subparsers) add_scrub_job_records_arg(subparsers) add_export_db_arg(subparsers) add_pop_db_arg(subparsers) @@ -805,6 +816,11 @@ def select_accounting_function(args, output_file, parser): "project": args.project, } return_val = flux.Flux().rpc("accounting.delete_project", data).get() + elif args.func == "list_projects": + data = { + "path": args.path, + } + return_val = flux.Flux().rpc("accounting.list_projects", data).get() elif args.func == "scrub_old_jobs": data = { "path": args.path, diff --git a/t/t1025-flux-account-projects.t b/t/t1025-flux-account-projects.t index 97853dab5..b39e13073 100755 --- a/t/t1025-flux-account-projects.t +++ b/t/t1025-flux-account-projects.t @@ -43,6 +43,16 @@ test_expect_success 'add some queues to the DB' ' flux account add-queue special --priority=99999 ' +test_expect_success 'list contents of project_table before adding projects' ' + flux account list-projects > project_table.test && + cat <<-EOF >project_table.expected + project_id | project | usage + -----------+---------+------ + 1 | * | 0.0 + EOF + grep -f project_table.expected project_table.test +' + test_expect_success 'add some projects to the project_table' ' flux account add-project project_1 && flux account add-project project_2 && @@ -125,6 +135,18 @@ test_expect_success 'edit the default project of a user' ' grep -f projects_list.expected edited_default_project.test ' +test_expect_success 'list all of the projects currently registered in project_table' ' + flux account list-projects > project_table.test && + cat <<-EOF >project_table.expected + project_id | project | usage + -----------+-----------+------ + 1 | * | 0.0 + 3 | project_2 | 0.0 + 4 | project_3 | 0.0 + EOF + grep -f project_table.expected project_table.test +' + test_expect_success 'remove flux-accounting DB' ' rm $(pwd)/FluxAccountingTest.db '