This commit is contained in:
Michael Woods
2025-02-19 15:01:14 -05:00
parent a74ed0c447
commit 92509b10ab
3 changed files with 112 additions and 14 deletions

View File

@@ -5,6 +5,7 @@ from packetserver.client import Client
from packetserver.common.constants import yes_values from packetserver.common.constants import yes_values
from packetserver.common import Request, Response from packetserver.common import Request, Response
from packetserver.client.cli.util import format_list_dicts, exit_client from packetserver.client.cli.util import format_list_dicts, exit_client
from packetserver.client.cli.job import job
import ZODB import ZODB
import ZODB.FileStorage import ZODB.FileStorage
import ax25 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('--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('--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('--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.version_option(VERSION,"--version", "-v")
@click.pass_context @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.""" """Command line interface for the PacketServer client and server API."""
ctx.ensure_object(dict) ctx.ensure_object(dict)
cfg = get_config(config_file_path=conf) 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() != '': if callsign.strip() != '':
ctx.obj['callsign'] = callsign.strip().upper() ctx.obj['callsign'] = callsign.strip().upper()
else: else:
if 'callsign' in cfg['cli']: if 'callsign' in cfg['cli']:
ctx.obj['callsign'] = cfg['cli']['callsign'] ctx.obj['callsign'] = cfg['cli']['callsign']
else: else:
click.echo("You must provide client station's callsign.", err=True) ctx.obj['callsign'] = click.prompt('Please enter your station callsign (with ssid if needed)', type=str)
sys.exit(1)
if not ax25.Address.valid_call(ctx.obj['callsign']): if not ax25.Address.valid_call(ctx.obj['callsign']):
click.echo(f"Provided client callsign '{ctx.obj['callsign']}' is invalid.", err=True) 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']: if 'server' in cfg['cli']:
ctx.obj['server'] = cfg['cli']['server'] ctx.obj['server'] = cfg['cli']['server']
else: else:
click.echo("Remote BBS server callsign must be specified.", err=True) ctx.obj['server'] = click.prompt('Please enter the bbs station callsign (with ssid if needed)', type=str)
sys.exit(1)
if not ax25.Address.valid_call(ctx.obj['server']): if not ax25.Address.valid_call(ctx.obj['server']):
click.echo(f"Provided remote server callsign '{ctx.obj['server']}' is invalid.", err=True) 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)) storage = ZODB.FileStorage.FileStorage(os.path.join(cfg['cli']['directory'], DEFAULT_DB_FILE))
db = ZODB.DB(storage) 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: try:
client.start() client.start()
except Exception as e: except Exception as e:
@@ -99,13 +106,13 @@ def query_server(ctx):
resp = client.send_receive_callsign(req, ctx.obj['bbs']) resp = client.send_receive_callsign(req, ctx.obj['bbs'])
if resp is None: if resp is None:
click.echo(f"No response from {ctx.obj['bbs']}") click.echo(f"No response from {ctx.obj['bbs']}")
exit_client(client, 1) exit_client(ctx.obj, 1)
else: else:
if resp.status_code != 200: 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: else:
click.echo(json.dumps(resp.payload, indent=2)) click.echo(json.dumps(resp.payload, indent=2))
exit_client(client, 0) exit_client(ctx.obj, 0)
@click.command() @click.command()
@@ -119,10 +126,10 @@ def user(ctx, list_users, output_format, username):
client = ctx.obj['client'] client = ctx.obj['client']
# validate args # validate args
if list_users and (username.strip() != ""): 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() == ""): 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 = [] output_objects = []
try: try:
@@ -131,15 +138,16 @@ def user(ctx, list_users, output_format, username):
else: else:
output_objects.append(users.get_user_by_username(client, ctx.obj['bbs'], username)) output_objects.append(users.get_user_by_username(client, ctx.obj['bbs'], username))
except Exception as e: except Exception as e:
exit_client(client,1, str(e)) exit_client(ctx.obj,1, str(e))
finally: finally:
client.stop() client.stop()
click.echo(format_list_dicts([x.pretty_dict() for x in output_objects], output_format=output_format.lower())) 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(user)
cli.add_command(query_server) cli.add_command(query_server)
cli.add_command(job, name='job')
if __name__ == '__main__': if __name__ == '__main__':
cli() cli()

View File

@@ -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)

View File

@@ -3,6 +3,9 @@ import json
import click import click
from packetserver.client import Client from packetserver.client import Client
import sys import sys
import ZODB
from persistent.mapping import PersistentMapping
import datetime
def format_list_dicts(dicts: list[dict], output_format: str = "table") -> str: def format_list_dicts(dicts: list[dict], output_format: str = "table") -> str:
if output_format == "table": if output_format == "table":
@@ -22,7 +25,24 @@ def format_list_dicts(dicts: list[dict], output_format: str = "table") -> str:
else: else:
raise ValueError("Unsupported format type.") 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() client.stop()
if return_code == 0: if return_code == 0:
is_err = False is_err = False