diff --git a/TODO.txt b/TODO.txt index 66d7aac..adaa4cd 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,5 +1,9 @@ TODO -- fix handling of control-c +- We should throw out fabric; paramiko does everything we need - we can open a shell and send signals to handle control-C + and other sorts of situation handling. We'll basically have to rewrite how fabtools works but it should be relatively + trivial since we already basically handle everything at the paramiko level. We may have to parse sshconfig ourselves + but it shouldn't be too big a deal. +- fix handling of control-c (sorta works sometimes) - redo the command loop to make some fuckin sense why do we use eceptions to do modal stuff - make styles optional - expose styles in config file or personal config @@ -11,7 +15,6 @@ TODO - add commands to show the list of captured outputs, along with the commands and targets - add commands to search captured outputs - add commands to search captured outputs and put them into a variable that can be used for /target etc. -- calling commands and aliases from command line (e.g. multiball -t -c ) - tagging and filtering commands (so `noupgrade` gets a tag, and we can filter them) - saving the last host set in a .file - allow server groups / server labels @@ -26,5 +29,9 @@ TODO - implement interactive alias system - Catch more exceptions in fabtools, and also add retries - Make the runner aware of multiple commands so that it can combine outputs and make 'overall success' or 'overall failure' -- make C-c break the connections not the program +- make C-c break the connections not the program (this requires throwing out fabric) - make a /summary command that *only* outputs the summary of a remote command, not the stdout/stderr +- document more things about multiball +- allow a user-specific configuration that isn't in the current directory +- make a set of commands that modulate the /all list, so /all goes back to the real default not the total default +- the ability to deploy files diff --git a/multiball/__init__.py b/multiball/__init__.py index 906d362..49e0fc1 100644 --- a/multiball/__init__.py +++ b/multiball/__init__.py @@ -1 +1 @@ -__version__ = "0.6.0" +__version__ = "0.7.0" diff --git a/multiball/__main__.py b/multiball/__main__.py index 7cd6279..2d18f85 100644 --- a/multiball/__main__.py +++ b/multiball/__main__.py @@ -31,6 +31,7 @@ from .fabtools import HostSet from .tools import (load_bare_server_list, load_config, load_sshconfig_server_list) +oprint = print from .style import print, STYLES class MultiballCommandException(BaseException): @@ -116,13 +117,13 @@ class Multiball: self.ssh_config = Path(sc).expanduser() newhosts = load_sshconfig_server_list( Path(sc).expanduser()) - print(f"Loaded {len(newhosts)} from {sc}") + print(f"Loaded {len(newhosts)} host(s) from {sc}") self.allhosts.update(newhosts) if bareconfigs := config.get('hostlists'): for sc in bareconfigs.split(','): newhosts = load_bare_server_list(Path(sc).expanduser()) - print(f"Loaded {len(newhosts)} from {sc}") + print(f"Loaded {len(newhosts)} host(s) from {sc}") self.allhosts.update(newhosts) else: @@ -417,12 +418,59 @@ class Multiball: # Nothing of importance happens here. ... +def parse_args(argv): + if (len(argv) > 1): + parser = argparse.ArgumentParser( + prog='multiball', + description='Run commands on a number of hosts.', + epilog='With no arguments enter interactive mode.', + ) + parser.add_argument('--confirm', action='store_true', help='Skip confirmation step, use with caution.') + parser.add_argument('-t', '--target', type=str, nargs='?', action='append', help='Select one or more targets.') + parser.add_argument('-a', '--all', action='store_true', help='Select default target list.') + parser.add_argument('-c', '--command', type=str, nargs='+', action='append', help='Command to run.') + res = parser.parse_args(argv) + return res + else: + return None -def multiball(*args, **kwargs): + +def multiball(argv): + args = parse_args(argv) + + ## Command mode + if (args): + if (not args.all and (len(args.target) <= 0)): + print('Need either --all or at least one --target') + return 1 + if (len(args.command) <= 0): + print('Need at least one --command') + return 1 + + print(f"Multiball {__version__}.") + mb = Multiball() + + if (args.all): + mb.targethosts = mb.allhosts + else: + mb.targethosts = args.target + + for command in args.command: + mb._print_targetlist() + command = ' '.join(command) + if (not args.confirm): + try: + mb.command_confirm('', args=f"Run `{command}`?") + except MBCommandAbort: + continue + mb._run_remote_command(command) + return 0 + + ## Interactive mode print(f"Welcome to Multiball {__version__}. type /help for help.") mb = Multiball() - # loop on prompt while (True): + # loop on prompt try: mb.cmd_prompt() except MBExit: @@ -431,7 +479,7 @@ def multiball(*args, **kwargs): def main(): try: - sys.exit(multiball(sys.argv)) + sys.exit(multiball(sys.argv[1:])) except EOFError: sys.exit(0) except Exception as inst: diff --git a/multiball/style.py b/multiball/style.py index 927951c..5a4e58b 100644 --- a/multiball/style.py +++ b/multiball/style.py @@ -19,6 +19,7 @@ STYLES = Style.from_dict({ def print(message): + message = message.replace("&", "&") print_formatted_text(HTML(f"{message}"), style=STYLES)