Compare commits
No commits in common. "trunk" and "trunk" have entirely different histories.
26
README.txt
26
README.txt
@ -7,29 +7,9 @@
|
|||||||
Run commands across a set of hosts interactively!
|
Run commands across a set of hosts interactively!
|
||||||
=================================================
|
=================================================
|
||||||
|
|
||||||
Basic configuration file is in `multiball.cfg.example` in the distribution. Customize to your liking.
|
Basic configuration file is in multiball.cfg.example in the distribution. Customize to your liking. Host list is loaded from,
|
||||||
|
by default, your ssh configuration (assuming you have a specific ssh configuration for your all hosts like Autonomic has). You
|
||||||
|
can also use arbitrary host lists (documented in configuration file).
|
||||||
|
|
||||||
Host list is loaded from your ssh configuration by default (assuming you have a specific ssh configuration for your all hosts like Autonomic has). You can also use arbitrary host lists (documented in configuration file).
|
|
||||||
|
|
||||||
Planned features, see __main__.py's comments.
|
Planned features, see __main__.py's comments.
|
||||||
|
|
||||||
Installation
|
|
||||||
-------------------------------------------------
|
|
||||||
|
|
||||||
Install dependencies:
|
|
||||||
|
|
||||||
* Debian: `sudo apt install python3 python3-venv git make`
|
|
||||||
* Fedora: `sudo dnf install python3 python3-pip git make`
|
|
||||||
|
|
||||||
Clone the repo:
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone https://git.autonomic.zone/autonomic-cooperative/multiball.git
|
|
||||||
```
|
|
||||||
|
|
||||||
Install, including dependencies:
|
|
||||||
|
|
||||||
```
|
|
||||||
cd multiball
|
|
||||||
make
|
|
||||||
```
|
|
||||||
|
23
TODO.txt
23
TODO.txt
@ -1,23 +0,0 @@
|
|||||||
TODO
|
|
||||||
- keep track of previous command outputs for last command, and any previous command with target list and command line
|
|
||||||
- calling commands and alieses from command line
|
|
||||||
- tagging and filtering commands (so oupgrade gets a tag, and we can filter them)
|
|
||||||
- saving the last host set in a .file
|
|
||||||
- allow server groups
|
|
||||||
- allow grepping of output from a command, and then populating the target list from matchintg on it
|
|
||||||
- add 'watch' to run a command repeatedly until it succeeds (interruptable)
|
|
||||||
- add the number of servers for each group of services too
|
|
||||||
- notice when you don't sudo in front of command and it errors and ask you if you meant to sudo
|
|
||||||
- autocomplete for environment variable names (requires heirarchical completer)
|
|
||||||
- implement target aliases (-filteralias) which gives a label to an argument to -hosts
|
|
||||||
- ad-hoc host groupings with assigned names, and a host grouping stack
|
|
||||||
- Assign each output group to a number that can be easily selected with a -target etc. But also allow matching
|
|
||||||
against those groups. Output groupings survive until the next command.
|
|
||||||
- allow scripts that use -safe to prompt for safety / restore safety after running
|
|
||||||
- Add variables that can be set and passed to commands
|
|
||||||
- implement various commented commands in the command list
|
|
||||||
- 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 a /summary command that *only* outputs the summary of a remote command, not the stdout/stderr
|
|
21
makefile
21
makefile
@ -1,21 +0,0 @@
|
|||||||
default: install
|
|
||||||
|
|
||||||
.venv:
|
|
||||||
python3 -m venv .venv
|
|
||||||
|
|
||||||
pip: .venv
|
|
||||||
@.venv/bin/pip install -e .
|
|
||||||
|
|
||||||
$(HOME)/.local/bin/:
|
|
||||||
@echo "'$(HOME)/.local/bin' was not found; creating it"
|
|
||||||
mkdir -p $(HOME)/.local/bin
|
|
||||||
@echo 'Be sure to add this directory to your $$PATH, using e.g.'
|
|
||||||
@echo "echo $(HOME)/.local/bin >> $(HOME)/.bashrc"
|
|
||||||
|
|
||||||
$(HOME)/.local/bin/multiball: $(HOME)/.local/bin/ pip
|
|
||||||
ln -s $(PWD)/.venv/bin/multiball $(HOME)/.local/bin/multiball
|
|
||||||
|
|
||||||
install: $(HOME)/.local/bin/multiball
|
|
||||||
|
|
||||||
.PHONY: pip install
|
|
||||||
|
|
@ -3,6 +3,27 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
#
|
#
|
||||||
|
# TODO
|
||||||
|
# - keep track of previous command outputs for last command, and any previous command with target list and command line
|
||||||
|
# - calling commands and alieses from command line
|
||||||
|
# - 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
|
||||||
|
# - allow grepping of output from a command, and then populating the target list from matchintg on it
|
||||||
|
# - add 'watch' to run a command repeatedly until it succeeds (interruptable)
|
||||||
|
# - add the number of servers for each group of services too
|
||||||
|
# - notice when you don't sudo in front of command and it errors and ask you if you meant to sudo
|
||||||
|
# - autocomplete for environment variable names (requires heirarchical completer)
|
||||||
|
# - implement target aliases (-filteralias) which gives a label to an argument to -hosts
|
||||||
|
# - ad-hoc host groupings with assigned names, and a host grouping stack
|
||||||
|
# - Assign each output group to a number that can be easily selected with a -target etc. But also allow matching
|
||||||
|
# against those groups. Output groupings survive until the next command.
|
||||||
|
# - allow scripts that use -safe to prompt for safety / restore safety after running
|
||||||
|
# - Add variables that can be set and passed to commands
|
||||||
|
# - implement various commented commands in the command list
|
||||||
|
# - implement interactive alias system
|
||||||
|
# - Catch more exceptions in fabtools, and also add retries
|
||||||
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import datetime
|
import datetime
|
||||||
|
@ -1,39 +1,22 @@
|
|||||||
import socket
|
|
||||||
import time
|
import time
|
||||||
from enum import Enum
|
|
||||||
from pathlib import Path
|
|
||||||
from threading import Lock, Thread
|
|
||||||
|
|
||||||
import paramiko
|
from pathlib import Path
|
||||||
from fabric2 import Config, Connection, SerialGroup, ThreadingGroup
|
from threading import Thread, Lock
|
||||||
|
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
from fabric2 import ThreadingGroup, SerialGroup, Config, Connection
|
||||||
|
|
||||||
|
import paramiko
|
||||||
|
|
||||||
class RunResult(Enum):
|
|
||||||
Success = 0
|
|
||||||
CommandFailure = 1
|
|
||||||
Error = 2
|
|
||||||
Cancelled = 3
|
|
||||||
Timeout = 4 # FIXME support host timeouts
|
|
||||||
|
|
||||||
def thread_run(connection, command, result_lock, result_queue):
|
def thread_run(connection, command, result_lock, result_queue):
|
||||||
runresult = RunResult.Success
|
|
||||||
try:
|
try:
|
||||||
res = connection.run(command, warn=True, hide=True)
|
res = connection.run(command, warn=True, hide=True)
|
||||||
if (res.exited != 0):
|
except (paramiko.ssh_exception.NoValidConnectionsError, paramiko.ssh_exception.SSHException) as inst:
|
||||||
runresult = RunResult.CommandFailure
|
|
||||||
except (paramiko.ssh_exception.NoValidConnectionsError, paramiko.ssh_exception.SSHException, socket.gaierror) as inst:
|
|
||||||
res = f"Could not connect to host: {inst}"
|
res = f"Could not connect to host: {inst}"
|
||||||
runresult = RunResult.Error
|
|
||||||
except KeyboardInterrupt as inst:
|
|
||||||
res = f"Canceled host {inst} from C-c"
|
|
||||||
runresult = RunResult.Cancelled
|
|
||||||
except Exception as inst:
|
|
||||||
res = f"Unknown error for host: {inst}"
|
|
||||||
runresult = RunResult.Error
|
|
||||||
with result_lock:
|
with result_lock:
|
||||||
result_queue.append((connection, res, runresult))
|
result_queue.append((connection, res))
|
||||||
|
|
||||||
|
|
||||||
# A set of hosts we can target with a series of commands and also can collect output of each command
|
# A set of hosts we can target with a series of commands and also can collect output of each command
|
||||||
class HostSet:
|
class HostSet:
|
||||||
@ -75,37 +58,24 @@ class HostSet:
|
|||||||
if ((time.time() - tupdate) > 10.0):
|
if ((time.time() - tupdate) > 10.0):
|
||||||
tupdate = time.time()
|
tupdate = time.time()
|
||||||
print("Still waiting on ", [thread.host for thread in threads])
|
print("Still waiting on ", [thread.host for thread in threads])
|
||||||
# FIXME have the thread killed if it's still waiting after $TIMEOUT
|
|
||||||
prog.close()
|
prog.close()
|
||||||
|
|
||||||
# Gather up results by output
|
# Gather up results by output
|
||||||
gathered = {}
|
gathered = {}
|
||||||
success = []
|
for connection, result in resq:
|
||||||
error = []
|
|
||||||
commandfail = []
|
|
||||||
|
|
||||||
for connection, result, runresult in resq:
|
|
||||||
# print(connection, result, runresult)
|
|
||||||
rstr = str(result)
|
rstr = str(result)
|
||||||
if not rstr in gathered:
|
if not rstr in gathered:
|
||||||
gathered[rstr] = []
|
gathered[rstr] = []
|
||||||
gathered[rstr].append(connection)
|
gathered[rstr].append(connection)
|
||||||
if runresult in (RunResult.Error, RunResult.Cancelled):
|
|
||||||
error.append(connection)
|
|
||||||
elif runresult == RunResult.CommandFailure:
|
|
||||||
commandfail.append(connection)
|
|
||||||
else:
|
|
||||||
success.append(connection)
|
|
||||||
|
|
||||||
# display results
|
# display results
|
||||||
for result, connections in gathered.items():
|
for result, connections in gathered.items():
|
||||||
print('-----> [{}]'.format(' '.join(connection.original_host for connection in connections)))
|
print('-----> [{}]'.format(' '.join(connection.original_host for connection in connections)))
|
||||||
print(result)
|
print(result)
|
||||||
|
# ## gather_output
|
||||||
print('#####> RUN SUMMARY <#####')
|
# for connection in self.connections:
|
||||||
if (success):
|
# # import pdb
|
||||||
print(' Succeeded: ', ', '.join(connection.original_host for connection in success))
|
# # pdb.set_trace()
|
||||||
if (commandfail):
|
# res = connection.run(command)
|
||||||
print(' Command fail: ', ', '.join(connection.original_host for connection in commandfail))
|
# print('---- {} ----'.format(connection.original_host))
|
||||||
if (error):
|
# print(res)
|
||||||
print('Failed with Error: ', ','.join(connection.original_host for connection in error))
|
|
||||||
|
Loading…
Reference in New Issue
Block a user