diff --git a/src/packetserver/client/cli/__init__.py b/src/packetserver/client/cli/__init__.py index fa6653d..e956a08 100644 --- a/src/packetserver/client/cli/__init__.py +++ b/src/packetserver/client/cli/__init__.py @@ -5,6 +5,7 @@ from packetserver.client import Client from packetserver.common.constants import yes_values from packetserver.common import Request, Response from packetserver.client.cli.util import format_list_dicts, exit_client +from packetserver.client.cli.job import job import ZODB import ZODB.FileStorage import ax25 @@ -25,21 +26,28 @@ VERSION="0.1.0-alpha" @click.option('--agwpe', '-a', default='', help="AGWPE TNC server address to connect to (config file)") @click.option('--port', '-p', default=0, help="AGWPE TNC server port to connect to (config file)") @click.option('--callsign', '-c', default='', help="radio callsign[+ssid] of this client station (config file)") +@click.option('--keep-log', '-k', is_flag=True, default=False, help="Save local copy of request log after session ends?") @click.version_option(VERSION,"--version", "-v") @click.pass_context -def cli(ctx, conf, server, agwpe, port, callsign): +def cli(ctx, conf, server, agwpe, port, callsign, keep_log): """Command line interface for the PacketServer client and server API.""" ctx.ensure_object(dict) cfg = get_config(config_file_path=conf) + ctx.obj['keep_log'] = False + if keep_log: + ctx.obj['keep_log'] = True + else: + if cfg['cli'].get('keep_log', fallback='n') in yes_values: + ctx.obj['keep_log'] = True + if callsign.strip() != '': ctx.obj['callsign'] = callsign.strip().upper() else: if 'callsign' in cfg['cli']: ctx.obj['callsign'] = cfg['cli']['callsign'] else: - click.echo("You must provide client station's callsign.", err=True) - sys.exit(1) + ctx.obj['callsign'] = click.prompt('Please enter your station callsign (with ssid if needed)', type=str) if not ax25.Address.valid_call(ctx.obj['callsign']): click.echo(f"Provided client callsign '{ctx.obj['callsign']}' is invalid.", err=True) @@ -51,8 +59,7 @@ def cli(ctx, conf, server, agwpe, port, callsign): if 'server' in cfg['cli']: ctx.obj['server'] = cfg['cli']['server'] else: - click.echo("Remote BBS server callsign must be specified.", err=True) - sys.exit(1) + ctx.obj['server'] = click.prompt('Please enter the bbs station callsign (with ssid if needed)', type=str) if not ax25.Address.valid_call(ctx.obj['server']): click.echo(f"Provided remote server callsign '{ctx.obj['server']}' is invalid.", err=True) @@ -76,7 +83,7 @@ def cli(ctx, conf, server, agwpe, port, callsign): storage = ZODB.FileStorage.FileStorage(os.path.join(cfg['cli']['directory'], DEFAULT_DB_FILE)) db = ZODB.DB(storage) - client = Client(ctx.obj['agwpe_server'], ctx.obj['port'], ctx.obj['callsign'], keep_log=True) + client = Client(ctx.obj['agwpe_server'], ctx.obj['port'], ctx.obj['callsign'], keep_log=ctx.obj['keep_log']) try: client.start() except Exception as e: @@ -99,13 +106,13 @@ def query_server(ctx): resp = client.send_receive_callsign(req, ctx.obj['bbs']) if resp is None: click.echo(f"No response from {ctx.obj['bbs']}") - exit_client(client, 1) + exit_client(ctx.obj, 1) else: if resp.status_code != 200: - exit_client(client, 1, message=f"Error contacting server: {resp.payload}") + exit_client(ctx.obj, 1, message=f"Error contacting server: {resp.payload}") else: click.echo(json.dumps(resp.payload, indent=2)) - exit_client(client, 0) + exit_client(ctx.obj, 0) @click.command() @@ -119,10 +126,10 @@ def user(ctx, list_users, output_format, username): client = ctx.obj['client'] # validate args if list_users and (username.strip() != ""): - exit_client(client,1, "Can't specify a username while listing all users.") + exit_client(ctx.obj,1, "Can't specify a username while listing all users.") if not list_users and (username.strip() == ""): - exit_client(client,1, message="Must provide either a username or --list-users flag.") + exit_client(ctx.obj,1, message="Must provide either a username or --list-users flag.") output_objects = [] try: @@ -131,15 +138,16 @@ def user(ctx, list_users, output_format, username): else: output_objects.append(users.get_user_by_username(client, ctx.obj['bbs'], username)) except Exception as e: - exit_client(client,1, str(e)) + exit_client(ctx.obj,1, str(e)) finally: client.stop() click.echo(format_list_dicts([x.pretty_dict() for x in output_objects], output_format=output_format.lower())) - exit_client(client, 0) + exit_client(ctx.obj, 0) cli.add_command(user) cli.add_command(query_server) +cli.add_command(job, name='job') if __name__ == '__main__': cli() diff --git a/src/packetserver/client/cli/job.py b/src/packetserver/client/cli/job.py index e69de29..952c7b2 100644 --- a/src/packetserver/client/cli/job.py +++ b/src/packetserver/client/cli/job.py @@ -0,0 +1,70 @@ +"""CLI client for dealing with jobs.""" +import os + +import click +from persistent.mapping import default +from packetserver.client import Client +from packetserver.client.jobs import JobSession +import datetime +from packetserver.client.cli.util import exit_client + +@click.group() +@click.pass_context +def job(ctx): + """Runs commands on the BBS server if jobs are enabled on it.""" + pass + +@click.command() +@click.pass_context +def start(): + """Start a job on the BBS server.""" + pass + +@click.command() +@click.pass_context +def get(): + """Retrieve a job""" + pass + +@click.command() +@click.option("--transcript", "-T", default="", help="File to write command transcript to if desired.") +@click.pass_context +def quick_session(ctx, transcript): + """Start a session to submit multiple commands and receive responses immediately""" + session_transcript = [] + client = ctx.obj['client'] + bbs = ctx.obj['bbs'] + js = JobSession(client, bbs, stutter=2) + db_enabled = True + while True: + cmd = click.prompt("CMD", prompt_suffix=" >") + cmd = cmd.strip() + session_transcript = session_transcript.append((datetime.datetime.now(),"c",cmd)) + next_db = False + if db_enabled: + next_db = True + db_enabled = False + if cmd == "": + continue + if cmd == "/exit": + break + elif cmd == "/db": + click.echo("DB requested for next command.") + db_enabled = True + else: + try: + job_result = js.send_quick(['bash', '-c', cmd], db=next_db) + output = job_result.output_str + "\n" + "Errors: " + job_result.errors_str + session_transcript = session_transcript.append((datetime.datetime.now(), "r", output)) + click.echo(output) + except Exception as e: + session_transcript = session_transcript.append((datetime.datetime.now(), "e", e)) + click.echo(f"ERROR! {str(e)}", err=True) + continue + try: + if transcript.strip() != "": + with open(transcript.strip(), 'w') as tran_file: + for l in session_transcript: + tran_file.write(f"{l[1]}:{l[0].isoformat()}: {l[2]}{os.linesep}") + finally: + exit_client(ctx.obj, 0) diff --git a/src/packetserver/client/cli/util.py b/src/packetserver/client/cli/util.py index 726f4af..683cdee 100644 --- a/src/packetserver/client/cli/util.py +++ b/src/packetserver/client/cli/util.py @@ -3,6 +3,9 @@ import json import click from packetserver.client import Client import sys +import ZODB +from persistent.mapping import PersistentMapping +import datetime def format_list_dicts(dicts: list[dict], output_format: str = "table") -> str: if output_format == "table": @@ -22,7 +25,24 @@ def format_list_dicts(dicts: list[dict], output_format: str = "table") -> str: else: raise ValueError("Unsupported format type.") -def exit_client(client: Client, return_code: int, message=""): +def write_request_log(db: ZODB.DB, client: Client): + with db.transaction() as db_trans: + if not 'request_log' in db_trans.root(): + db_trans['request_log'] = PersistentMapping() + now = datetime.datetime.now() + db_trans['request_log'][now.isoformat()] = client.request_log + + + +def exit_client(context: dict, return_code: int, message=""): + client = context['client'] + db = context['db'] + client.stop() + + if context['keep_log']: + write_request_log(db, client) + + db.close() client.stop() if return_code == 0: is_err = False