Add the start of output logging!

bumped to version 0.5.0.

- Fix some typos.
- Make fabtools HostSet save the command output to itself.
- Make the multiball runner save the most recent output.
- (command: /save) Add command to save most recent output to file.
- Add a debug command for development purposes.
This commit is contained in:
2025-08-12 11:20:21 -07:00
parent 9e76baf49c
commit 99f5bff2d8
3 changed files with 52 additions and 12 deletions

View File

@ -1 +1 @@
__version__ = "0.0.0"
__version__ = "0.5.0"

View File

@ -25,6 +25,7 @@ from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.history import InMemoryHistory, FileHistory
from prompt_toolkit.shortcuts import PromptSession
from . import __version__
from .fabtools import HostSet
from .tools import load_sshconfig_server_list, load_bare_server_list, load_config
@ -64,17 +65,19 @@ COMMANDS = {
'clearenv': (('/clearenv',), "Clear environment (entire or single variable)"),
'safety': (('/safety', 'safety'), "Turn on safety or turn off with `off`."),
'safe': (('/safe', ), "Run this command as if it were safe even if safety is on."),
'confirm': (('/confirm',), "[scriptpfing] Prompt for confirmation"),
'confirm': (('/confirm',), "[scripting] Prompt for confirmation"),
'echo': (('/echo', ), "[scripting] print string"),
'arguments': (('/arguments',), "[scripting] Abort if no arguments are specified."),
# fixme add interpolation
'unsafe': (('/unsafe', ), "[scriptiong] Abort command if safety is on."),
'unsafe': (('/unsafe', ), "[scripting] Abort command if safety is on."),
'save': (('/save', ),"Save last run log to file."),
'debug': (('/////debug', ), "[devel] enter PDB"),
# 'history': (('/history', '/h'), "Show the last 10 commands in the history."),
# 'filteralias': (('/filteralias',), "Create a filter alias which allows quick filtering."),
# 'undo': (('/undo',), "Undo last host or environment command."),
# 'alias': (('/alias', ), "Create an alias interactively.")
## Fixme unambiguate the above commands which list things into this command so things are more consistent
# 'list': (('/list',), "List various things (environmet, allhosts, hosts, targets)")
# 'list': (('/list',), "List various things (environment, allhosts, hosts, targets)")
# '!': (('!', '/local'), "Run a local command and capture its output.")
}
@ -90,6 +93,7 @@ class Multiball:
'safety': self.command_safety,
'safe': self.command_safe,
'clearenv': self.command_clearenv,
'save': self.command_save,
}
configdict = load_config(Path("multiball.cfg"))
self.allhosts: set = set()
@ -97,6 +101,7 @@ class Multiball:
self.environment = {}
self.safety = True
self.ssh_config = None
self.last_run = {}
if config := configdict.get('config', {}):
if 'history' in config:
@ -236,6 +241,27 @@ class Multiball:
print("cleared environment")
self.environment = dict()
def command_save(self, command, args):
# FIXME: We need to make aliases store multiple entries, and save should take the whole alias not just
# the last command.
if (not self.last_run):
print("Nothing to log.")
return
tstamp = datetime.datetime.now().strftime("%Y-%m-%d-%H%M%Z")
outfilename = f"{tstamp}.multiball.log"
if (args):
outfilename = args[0]
if (not outfilename.endswith(".log")):
outfilename = outfilename + ".log"
print(f"Writing {outfilename} for command `{self.last_run['cmd']}`.")
with (open(outfilename, 'w')) as out:
out.writelines([f"command: {self.last_run['cmd']}\n",
"-------------------------------------------\n"
""])
out.write(self.last_run['results'].getvalue())
out.write("\n")
def _print_targetlist(self, tlist=None, keyword='Targeting'):
if not tlist:
tlist = self.targethosts
@ -267,8 +293,11 @@ class Multiball:
if abst == 'exit':
raise MBExit
if abst == 'help':
elif abst == 'help':
self._print_help()
elif abst == 'debug':
print("==== DEBUGGING ====")
__import__("pdb").set_trace()
elif abst in self.handlers:
self.handlers[abst](command[0], cargs)
elif abst == 'clear':
@ -342,6 +371,10 @@ class Multiball:
remotecmd.append(command)
h = HostSet(self.targethosts, self.ssh_config)
h.run(';'.join(remotecmd))
self.last_run = {
'cmd': remotecmd,
'results': h.results
}
def do_cmd(self, command):
try:
@ -376,7 +409,7 @@ class Multiball:
def multiball(*args, **kwargs):
print("Welcome to Multiball. type /help for help.")
print(f"Welcome to Multiball {__version__}. type /help for help.")
mb = Multiball()
# loop on prompt
while (True):

View File

@ -1,3 +1,4 @@
import io
import socket
import time
from enum import Enum
@ -37,7 +38,13 @@ def thread_run(connection, command, result_lock, result_queue):
# A set of hosts we can target with a series of commands and also can collect output of each command
class HostSet:
def _lprint(self, *args):
s = " ".join([str(a) for a in args]) + '\n'
self.results.write(s)
print(*args)
def __init__(self, hostlist: list, ssh_config_path: Path=None):
self.results = io.StringIO()
if ssh_config_path is None:
ssh_config_path = Path("~/.ssh/config")
ssh_config_path = ssh_config_path.expanduser()
@ -99,13 +106,13 @@ class HostSet:
# display results
for result, connections in gathered.items():
print('-----> [{}]'.format(' '.join(connection.original_host for connection in connections)))
print(result)
self._lprint('-----> [{}]'.format(' '.join(connection.original_host for connection in connections)))
self._lprint(result)
print('#####> RUN SUMMARY <#####')
self._lprint('#####> RUN SUMMARY <#####')
if (success):
print(' Succeeded: ', ', '.join(connection.original_host for connection in success))
self._lprint(' Succeeded: ', ', '.join(connection.original_host for connection in success))
if (commandfail):
print(' Command fail: ', ', '.join(connection.original_host for connection in commandfail))
self._lprint(' Command fail: ', ', '.join(connection.original_host for connection in commandfail))
if (error):
print('Failed with Error: ', ','.join(connection.original_host for connection in error))
self._lprint('Failed with Error: ', ','.join(connection.original_host for connection in error))