multiSSH3 6.16__tar.gz → 6.18__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {multissh3-6.16 → multissh3-6.18}/PKG-INFO +1 -1
- {multissh3-6.16 → multissh3-6.18}/multiSSH3.egg-info/PKG-INFO +1 -1
- {multissh3-6.16 → multissh3-6.18}/multiSSH3.py +220 -213
- {multissh3-6.16 → multissh3-6.18}/README.md +0 -0
- {multissh3-6.16 → multissh3-6.18}/multiSSH3.egg-info/SOURCES.txt +0 -0
- {multissh3-6.16 → multissh3-6.18}/multiSSH3.egg-info/dependency_links.txt +0 -0
- {multissh3-6.16 → multissh3-6.18}/multiSSH3.egg-info/entry_points.txt +0 -0
- {multissh3-6.16 → multissh3-6.18}/multiSSH3.egg-info/requires.txt +0 -0
- {multissh3-6.16 → multissh3-6.18}/multiSSH3.egg-info/top_level.txt +0 -0
- {multissh3-6.16 → multissh3-6.18}/setup.cfg +0 -0
- {multissh3-6.16 → multissh3-6.18}/setup.py +0 -0
- {multissh3-6.16 → multissh3-6.18}/test/test.py +0 -0
- {multissh3-6.16 → multissh3-6.18}/test/testCurses.py +0 -0
- {multissh3-6.16 → multissh3-6.18}/test/testCursesOld.py +0 -0
- {multissh3-6.16 → multissh3-6.18}/test/testPerfCompact.py +0 -0
- {multissh3-6.16 → multissh3-6.18}/test/testPerfExpand.py +0 -0
|
@@ -34,20 +34,114 @@ from collections import Counter, defaultdict, deque
|
|
|
34
34
|
from itertools import count, product
|
|
35
35
|
from pprint import pformat
|
|
36
36
|
|
|
37
|
-
__curses_available = False
|
|
38
|
-
__resource_lib_available = False
|
|
39
|
-
try:
|
|
40
|
-
import curses
|
|
41
|
-
import curses.panel
|
|
42
|
-
__curses_available = True
|
|
43
|
-
except ImportError:
|
|
44
|
-
pass
|
|
45
|
-
try:
|
|
46
|
-
import resource
|
|
47
|
-
__resource_lib_available = True
|
|
48
|
-
except ImportError:
|
|
49
|
-
pass
|
|
50
37
|
|
|
38
|
+
version = '6.18'
|
|
39
|
+
VERSION = version
|
|
40
|
+
__version__ = version
|
|
41
|
+
COMMIT_DATE = '2026-04-09'
|
|
42
|
+
# TODO: Add terminal TUI
|
|
43
|
+
|
|
44
|
+
CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
|
|
45
|
+
'~/multiSSH3.config.json',
|
|
46
|
+
'~/.multiSSH3.config.json',
|
|
47
|
+
'~/.config/multiSSH3/multiSSH3.config.json',
|
|
48
|
+
'/etc/multiSSH3.d/multiSSH3.config.json',
|
|
49
|
+
'/etc/multiSSH3.config.json'] # The first one has the highest priority
|
|
50
|
+
|
|
51
|
+
ERRORS = []
|
|
52
|
+
|
|
53
|
+
#%% ------------ User Defined Global Variables ----------------
|
|
54
|
+
AUTHOR = 'Yufei Pan'
|
|
55
|
+
AUTHOR_EMAIL = 'pan@zopyr.us'
|
|
56
|
+
DEFAULT_HOSTS = 'all'
|
|
57
|
+
DEFAULT_USERNAME = None
|
|
58
|
+
DEFAULT_PASSWORD = ''
|
|
59
|
+
DEFAULT_IDENTITY_FILE = None
|
|
60
|
+
DEFAULT_SSH_KEY_SEARCH_PATH = '~/.ssh/'
|
|
61
|
+
DEFAULT_USE_KEY = False
|
|
62
|
+
DEFAULT_EXTRA_ARGS = None
|
|
63
|
+
DEFAULT_ONE_ON_ONE = False
|
|
64
|
+
DEFAULT_SCP = False
|
|
65
|
+
DEFAULT_FILE_SYNC = False
|
|
66
|
+
DEFAULT_TIMEOUT = 50
|
|
67
|
+
DEFAULT_CLI_TIMEOUT = 0
|
|
68
|
+
DEFAULT_UNAVAILABLE_HOST_EXPIRY = 600
|
|
69
|
+
DEFAULT_REPEAT = 1
|
|
70
|
+
DEFAULT_INTERVAL = 0
|
|
71
|
+
DEFAULT_IPMI = False
|
|
72
|
+
DEFAULT_IPMI_INTERFACE_IP_PREFIX = ''
|
|
73
|
+
DEFAULT_INTERFACE_IP_PREFIX = None
|
|
74
|
+
DEFAULT_IPMI_USERNAME = 'ADMIN'
|
|
75
|
+
DEFAULT_IPMI_PASSWORD = ''
|
|
76
|
+
DEAFULT_IPMI_ARGS=''
|
|
77
|
+
DEFAULT_NO_WATCH = False
|
|
78
|
+
DEFAULT_WINDOW_WIDTH = 40
|
|
79
|
+
DEFAULT_WINDOW_HEIGHT = 1
|
|
80
|
+
DEFAULT_SINGLE_WINDOW = False
|
|
81
|
+
DEFAULT_ERROR_ONLY = False
|
|
82
|
+
DEFAULT_NO_OUTPUT = False
|
|
83
|
+
DEFAULT_RETURN_ZERO = False
|
|
84
|
+
DEFAULT_NO_ENV = False
|
|
85
|
+
DEFAULT_ENV_FILE = ''
|
|
86
|
+
DEFAULT_ENV_FILES = ['/etc/profile.d/hosts.sh',
|
|
87
|
+
'~/.bashrc',
|
|
88
|
+
'~/.zshrc',
|
|
89
|
+
'~/host.env',
|
|
90
|
+
'~/hosts.env',
|
|
91
|
+
'.env',
|
|
92
|
+
'host.env',
|
|
93
|
+
'hosts.env',
|
|
94
|
+
]
|
|
95
|
+
DEFAULT_NO_HISTORY = False
|
|
96
|
+
DEFAULT_HISTORY_FILE = '~/.mssh_history'
|
|
97
|
+
DEFAULT_MAX_CONNECTIONS = 4 * os.cpu_count()
|
|
98
|
+
DEFAULT_JSON_MODE = False
|
|
99
|
+
DEFAULT_PRINT_SUCCESS_HOSTS = False
|
|
100
|
+
DEFAULT_GREPPABLE_MODE = False
|
|
101
|
+
DEFAULT_SKIP_UNREACHABLE = True
|
|
102
|
+
DEFAULT_SKIP_HOSTS = ''
|
|
103
|
+
DEFAULT_HOST_FILE = '/etc/hosts'
|
|
104
|
+
DEFAULT_ENCODING = 'utf-8'
|
|
105
|
+
DEFAULT_DIFF_DISPLAY_THRESHOLD = 0.9
|
|
106
|
+
SSH_STRICT_HOST_KEY_CHECKING = False
|
|
107
|
+
FORCE_TRUECOLOR = False
|
|
108
|
+
ERROR_MESSAGES_TO_IGNORE = [
|
|
109
|
+
'Pseudo-terminal will not be allocated because stdin is not a terminal',
|
|
110
|
+
'Connection to .* closed',
|
|
111
|
+
'Warning: Permanently added',
|
|
112
|
+
'mux_client_request_session',
|
|
113
|
+
'disabling multiplexing',
|
|
114
|
+
'Killed by signal',
|
|
115
|
+
'Connection reset by peer',
|
|
116
|
+
]
|
|
117
|
+
POSSIBLE_SSH_KEY_FILES = ['id_ed25519','id_ed25519_sk','id_ecdsa','id_ecdsa_sk','id_rsa','id_dsa']
|
|
118
|
+
__DEFAULT_COLOR_PALETTE = {
|
|
119
|
+
'cyan': (86, 173, 188),
|
|
120
|
+
'green': (114, 180, 43),
|
|
121
|
+
'magenta': (140, 107, 200),
|
|
122
|
+
'red': (196, 38, 94),
|
|
123
|
+
'white': (227, 227, 221),
|
|
124
|
+
'yellow': (179, 180, 43),
|
|
125
|
+
'blue': (106, 126, 200),
|
|
126
|
+
'bright_black': (102, 102, 102),
|
|
127
|
+
'bright_blue': (129, 154, 255),
|
|
128
|
+
'bright_cyan': (102, 217, 239),
|
|
129
|
+
'bright_green': (126, 226, 46),
|
|
130
|
+
'bright_magenta': (174, 129, 255),
|
|
131
|
+
'bright_red': (249, 38, 114),
|
|
132
|
+
'bright_white': (248, 248, 242),
|
|
133
|
+
'bright_yellow': (226, 226, 46),
|
|
134
|
+
}
|
|
135
|
+
COLOR_PALETTE = __DEFAULT_COLOR_PALETTE.copy()
|
|
136
|
+
_DEFAULT_CALLED = True
|
|
137
|
+
_DEFAULT_RETURN_UNFINISHED = False
|
|
138
|
+
_DEFAULT_UPDATE_UNREACHABLE_HOSTS = True
|
|
139
|
+
_DEFAULT_NO_START = False
|
|
140
|
+
_etc_hosts = {}
|
|
141
|
+
__ERROR_MESSAGES_TO_IGNORE_REGEX =None
|
|
142
|
+
__DEBUG_MODE = False
|
|
143
|
+
|
|
144
|
+
#%% ------------ Pre Helper Functions ----------------
|
|
51
145
|
try:
|
|
52
146
|
# Check if functiools.cache is available
|
|
53
147
|
# cache_decorator = functools.cache
|
|
@@ -85,23 +179,7 @@ except Exception:
|
|
|
85
179
|
print('Warning: functools.lru_cache is not available, multiSSH3 will run slower without cache.',file=sys.stderr)
|
|
86
180
|
def cache_decorator(func):
|
|
87
181
|
return func
|
|
88
|
-
version = '6.16'
|
|
89
|
-
VERSION = version
|
|
90
|
-
__version__ = version
|
|
91
|
-
COMMIT_DATE = '2026-03-16'
|
|
92
|
-
|
|
93
|
-
CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
|
|
94
|
-
'~/multiSSH3.config.json',
|
|
95
|
-
'~/.multiSSH3.config.json',
|
|
96
|
-
'~/.config/multiSSH3/multiSSH3.config.json',
|
|
97
|
-
'/etc/multiSSH3.d/multiSSH3.config.json',
|
|
98
|
-
'/etc/multiSSH3.config.json'] # The first one has the highest priority
|
|
99
|
-
|
|
100
|
-
ERRORS = []
|
|
101
|
-
|
|
102
|
-
# TODO: Add terminal TUI
|
|
103
182
|
|
|
104
|
-
#%% ------------ Pre Helper Functions ----------------
|
|
105
183
|
def eprint(*args, **kwargs):
|
|
106
184
|
global ERRORS
|
|
107
185
|
try:
|
|
@@ -245,76 +323,6 @@ def _get_i():
|
|
|
245
323
|
'''
|
|
246
324
|
return next(_i_counter)
|
|
247
325
|
|
|
248
|
-
#%% ------------ Host Object ----------------
|
|
249
|
-
class Host:
|
|
250
|
-
def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False,identity_file=None,shell=False,i = -1,uuid=uuid.uuid4(),ip = None):
|
|
251
|
-
self.name = name # the name of the host (hostname or IP address)
|
|
252
|
-
self.command = command # the command to run on the host
|
|
253
|
-
self.returncode = None # the return code of the command
|
|
254
|
-
self.output = [] # the output of the command for curses
|
|
255
|
-
self.stdout = [] # the stdout of the command
|
|
256
|
-
self.stderr = [] # the stderr of the command
|
|
257
|
-
self.lineNumToPrintSet = set() # line numbers to reprint
|
|
258
|
-
self.lastUpdateTime = time.monotonic() # the last time the output was updated
|
|
259
|
-
self.lastPrintedUpdateTime = 0 # the last time the output was printed
|
|
260
|
-
self.files = files # the files to be copied to the host
|
|
261
|
-
self.ipmi = ipmi # whether to use ipmi to connect to the host
|
|
262
|
-
self.shell = shell # whether to use shell to run the command
|
|
263
|
-
self.interface_ip_prefix = interface_ip_prefix # the prefix of the ip address of the interface to be used to connect to the host
|
|
264
|
-
self.scp = scp # whether to use scp to copy files to the host
|
|
265
|
-
self.gatherMode = gatherMode # whether the host is in gather mode
|
|
266
|
-
self.extraargs = extraargs # extra arguments to be passed to ssh
|
|
267
|
-
self.resolvedName = None # the resolved IP address of the host
|
|
268
|
-
# also store a globally unique integer i from 0
|
|
269
|
-
self.i = i if i != -1 else _get_i()
|
|
270
|
-
self.uuid = uuid
|
|
271
|
-
self.identity_file = identity_file
|
|
272
|
-
self.ip = ip if ip else getIP(name)
|
|
273
|
-
self.current_color_pair = [-1, -1, 1]
|
|
274
|
-
self.output_buffer = io.BytesIO()
|
|
275
|
-
self.stdout_buffer = io.BytesIO()
|
|
276
|
-
self.stderr_buffer = io.BytesIO()
|
|
277
|
-
self.thread = None
|
|
278
|
-
|
|
279
|
-
def __iter__(self):
|
|
280
|
-
return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
|
|
281
|
-
def __repr__(self):
|
|
282
|
-
return f"Host(\n{pformat(self.__dict__)}\n)"
|
|
283
|
-
def __str__(self):
|
|
284
|
-
return f"Host(\n{pformat(dict(self))}\n)"
|
|
285
|
-
def get_output_hash(self):
|
|
286
|
-
return hash((
|
|
287
|
-
self.command,
|
|
288
|
-
tuple(self.stdout),
|
|
289
|
-
tuple(self.stderr),
|
|
290
|
-
self.returncode
|
|
291
|
-
))
|
|
292
|
-
def copy(self):
|
|
293
|
-
newHost = Host(
|
|
294
|
-
name=self.name,
|
|
295
|
-
command=self.command,
|
|
296
|
-
files=self.files,
|
|
297
|
-
ipmi=self.ipmi,
|
|
298
|
-
interface_ip_prefix=self.interface_ip_prefix,
|
|
299
|
-
scp=self.scp,
|
|
300
|
-
extraargs=self.extraargs,
|
|
301
|
-
gatherMode=self.gatherMode,
|
|
302
|
-
identity_file=self.identity_file,
|
|
303
|
-
shell=self.shell,
|
|
304
|
-
i=self.i,
|
|
305
|
-
uuid=self.uuid,
|
|
306
|
-
ip=self.ip
|
|
307
|
-
)
|
|
308
|
-
newHost.returncode = self.returncode
|
|
309
|
-
newHost.output = self.output.copy()
|
|
310
|
-
newHost.stdout = self.stdout.copy()
|
|
311
|
-
newHost.stderr = self.stderr.copy()
|
|
312
|
-
newHost.lineNumToPrintSet = self.lineNumToPrintSet.copy()
|
|
313
|
-
newHost.lastUpdateTime = self.lastUpdateTime
|
|
314
|
-
newHost.lastPrintedUpdateTime = self.lastPrintedUpdateTime
|
|
315
|
-
newHost.current_color_pair = self.current_color_pair.copy()
|
|
316
|
-
return newHost
|
|
317
|
-
|
|
318
326
|
#%% ------------ Load Defaults ( Config ) File ----------------
|
|
319
327
|
def load_config_file(config_file):
|
|
320
328
|
'''
|
|
@@ -336,96 +344,6 @@ def load_config_file(config_file):
|
|
|
336
344
|
return {}
|
|
337
345
|
return config
|
|
338
346
|
|
|
339
|
-
#%% ------------ Global Variables ----------------
|
|
340
|
-
AUTHOR = 'Yufei Pan'
|
|
341
|
-
AUTHOR_EMAIL = 'pan@zopyr.us'
|
|
342
|
-
DEFAULT_HOSTS = 'all'
|
|
343
|
-
DEFAULT_USERNAME = None
|
|
344
|
-
DEFAULT_PASSWORD = ''
|
|
345
|
-
DEFAULT_IDENTITY_FILE = None
|
|
346
|
-
DEFAULT_SSH_KEY_SEARCH_PATH = '~/.ssh/'
|
|
347
|
-
DEFAULT_USE_KEY = False
|
|
348
|
-
DEFAULT_EXTRA_ARGS = None
|
|
349
|
-
DEFAULT_ONE_ON_ONE = False
|
|
350
|
-
DEFAULT_SCP = False
|
|
351
|
-
DEFAULT_FILE_SYNC = False
|
|
352
|
-
DEFAULT_TIMEOUT = 50
|
|
353
|
-
DEFAULT_CLI_TIMEOUT = 0
|
|
354
|
-
DEFAULT_UNAVAILABLE_HOST_EXPIRY = 600
|
|
355
|
-
DEFAULT_REPEAT = 1
|
|
356
|
-
DEFAULT_INTERVAL = 0
|
|
357
|
-
DEFAULT_IPMI = False
|
|
358
|
-
DEFAULT_IPMI_INTERFACE_IP_PREFIX = ''
|
|
359
|
-
DEFAULT_INTERFACE_IP_PREFIX = None
|
|
360
|
-
DEFAULT_IPMI_USERNAME = 'ADMIN'
|
|
361
|
-
DEFAULT_IPMI_PASSWORD = ''
|
|
362
|
-
DEAFULT_IPMI_ARGS=''
|
|
363
|
-
DEFAULT_NO_WATCH = False
|
|
364
|
-
DEFAULT_WINDOW_WIDTH = 40
|
|
365
|
-
DEFAULT_WINDOW_HEIGHT = 1
|
|
366
|
-
DEFAULT_SINGLE_WINDOW = False
|
|
367
|
-
DEFAULT_ERROR_ONLY = False
|
|
368
|
-
DEFAULT_NO_OUTPUT = False
|
|
369
|
-
DEFAULT_RETURN_ZERO = False
|
|
370
|
-
DEFAULT_NO_ENV = False
|
|
371
|
-
DEFAULT_ENV_FILE = ''
|
|
372
|
-
DEFAULT_ENV_FILES = ['/etc/profile.d/hosts.sh',
|
|
373
|
-
'~/.bashrc',
|
|
374
|
-
'~/.zshrc',
|
|
375
|
-
'~/host.env',
|
|
376
|
-
'~/hosts.env',
|
|
377
|
-
'.env',
|
|
378
|
-
'host.env',
|
|
379
|
-
'hosts.env',
|
|
380
|
-
]
|
|
381
|
-
DEFAULT_NO_HISTORY = False
|
|
382
|
-
DEFAULT_HISTORY_FILE = '~/.mssh_history'
|
|
383
|
-
DEFAULT_MAX_CONNECTIONS = 4 * os.cpu_count()
|
|
384
|
-
DEFAULT_JSON_MODE = False
|
|
385
|
-
DEFAULT_PRINT_SUCCESS_HOSTS = False
|
|
386
|
-
DEFAULT_GREPPABLE_MODE = False
|
|
387
|
-
DEFAULT_SKIP_UNREACHABLE = True
|
|
388
|
-
DEFAULT_SKIP_HOSTS = ''
|
|
389
|
-
DEFAULT_HOST_FILE = '/etc/hosts'
|
|
390
|
-
DEFAULT_ENCODING = 'utf-8'
|
|
391
|
-
DEFAULT_DIFF_DISPLAY_THRESHOLD = 0.6
|
|
392
|
-
SSH_STRICT_HOST_KEY_CHECKING = False
|
|
393
|
-
FORCE_TRUECOLOR = False
|
|
394
|
-
ERROR_MESSAGES_TO_IGNORE = [
|
|
395
|
-
'Pseudo-terminal will not be allocated because stdin is not a terminal',
|
|
396
|
-
'Connection to .* closed',
|
|
397
|
-
'Warning: Permanently added',
|
|
398
|
-
'mux_client_request_session',
|
|
399
|
-
'disabling multiplexing',
|
|
400
|
-
'Killed by signal',
|
|
401
|
-
'Connection reset by peer',
|
|
402
|
-
]
|
|
403
|
-
__DEFAULT_COLOR_PALETTE = {
|
|
404
|
-
'cyan': (86, 173, 188),
|
|
405
|
-
'green': (114, 180, 43),
|
|
406
|
-
'magenta': (140, 107, 200),
|
|
407
|
-
'red': (196, 38, 94),
|
|
408
|
-
'white': (227, 227, 221),
|
|
409
|
-
'yellow': (179, 180, 43),
|
|
410
|
-
'blue': (106, 126, 200),
|
|
411
|
-
'bright_black': (102, 102, 102),
|
|
412
|
-
'bright_blue': (129, 154, 255),
|
|
413
|
-
'bright_cyan': (102, 217, 239),
|
|
414
|
-
'bright_green': (126, 226, 46),
|
|
415
|
-
'bright_magenta': (174, 129, 255),
|
|
416
|
-
'bright_red': (249, 38, 114),
|
|
417
|
-
'bright_white': (248, 248, 242),
|
|
418
|
-
'bright_yellow': (226, 226, 46),
|
|
419
|
-
}
|
|
420
|
-
COLOR_PALETTE = __DEFAULT_COLOR_PALETTE.copy()
|
|
421
|
-
_DEFAULT_CALLED = True
|
|
422
|
-
_DEFAULT_RETURN_UNFINISHED = False
|
|
423
|
-
_DEFAULT_UPDATE_UNREACHABLE_HOSTS = True
|
|
424
|
-
_DEFAULT_NO_START = False
|
|
425
|
-
_etc_hosts = {}
|
|
426
|
-
__ERROR_MESSAGES_TO_IGNORE_REGEX =None
|
|
427
|
-
__DEBUG_MODE = False
|
|
428
|
-
|
|
429
347
|
#%% Load Config Based Default Global variables
|
|
430
348
|
__configs_from_file = {}
|
|
431
349
|
for config_file in reversed(CONFIG_FILE_CHAIN.copy()):
|
|
@@ -467,12 +385,42 @@ __control_master_string = '''Host *
|
|
|
467
385
|
ControlPath /run/user/%i/ssh_sockets_%C
|
|
468
386
|
ControlPersist 3600
|
|
469
387
|
'''
|
|
388
|
+
__curses_available = False
|
|
389
|
+
__resource_lib_available = False
|
|
390
|
+
try:
|
|
391
|
+
import curses
|
|
392
|
+
import curses.panel
|
|
393
|
+
__curses_available = True
|
|
394
|
+
except ImportError:
|
|
395
|
+
pass
|
|
396
|
+
try:
|
|
397
|
+
import resource
|
|
398
|
+
__resource_lib_available = True
|
|
399
|
+
except ImportError:
|
|
400
|
+
pass
|
|
470
401
|
if __resource_lib_available:
|
|
471
402
|
# Get the current limits
|
|
472
403
|
_, __system_nofile_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
|
|
473
404
|
# Set the soft limit to the hard limit
|
|
474
405
|
resource.setrlimit(resource.RLIMIT_NOFILE, (__system_nofile_limit, __system_nofile_limit))
|
|
475
406
|
__max_connections_nofile_limit_supported = int((__system_nofile_limit - 10) / 3)
|
|
407
|
+
_binPaths = {}
|
|
408
|
+
_binCalled = set(['sshpass', 'ssh', 'scp', 'ipmitool','rsync','sh','ssh-copy-id'])
|
|
409
|
+
def check_path(program_name):
|
|
410
|
+
global __configs_from_file
|
|
411
|
+
global _binPaths
|
|
412
|
+
config_key = f'_{program_name}Path'
|
|
413
|
+
program_path = (
|
|
414
|
+
__configs_from_file.get(config_key) or
|
|
415
|
+
globals().get(config_key) or
|
|
416
|
+
shutil.which(program_name)
|
|
417
|
+
)
|
|
418
|
+
if program_path:
|
|
419
|
+
_binPaths[program_name] = program_path
|
|
420
|
+
return True
|
|
421
|
+
return False
|
|
422
|
+
|
|
423
|
+
[check_path(program) for program in _binCalled]
|
|
476
424
|
|
|
477
425
|
#%% Mapping of ANSI 4-bit colors to curses colors
|
|
478
426
|
if __curses_available:
|
|
@@ -494,26 +442,78 @@ if __curses_available:
|
|
|
494
442
|
96: curses.COLOR_CYAN, # Bright Cyan
|
|
495
443
|
97: curses.COLOR_WHITE # Bright White
|
|
496
444
|
}
|
|
497
|
-
#%% ------------ Exportable Help Functions ----------------
|
|
498
|
-
# check if command sshpass is available
|
|
499
|
-
_binPaths = {}
|
|
500
|
-
_binCalled = set(['sshpass', 'ssh', 'scp', 'ipmitool','rsync','sh','ssh-copy-id'])
|
|
501
|
-
def check_path(program_name):
|
|
502
|
-
global __configs_from_file
|
|
503
|
-
global _binPaths
|
|
504
|
-
config_key = f'_{program_name}Path'
|
|
505
|
-
program_path = (
|
|
506
|
-
__configs_from_file.get(config_key) or
|
|
507
|
-
globals().get(config_key) or
|
|
508
|
-
shutil.which(program_name)
|
|
509
|
-
)
|
|
510
|
-
if program_path:
|
|
511
|
-
_binPaths[program_name] = program_path
|
|
512
|
-
return True
|
|
513
|
-
return False
|
|
514
445
|
|
|
515
|
-
|
|
446
|
+
#%% ------------ Host Object ----------------
|
|
447
|
+
class Host:
|
|
448
|
+
def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False,identity_file=None,shell=False,i = -1,uuid=uuid.uuid4(),ip = None):
|
|
449
|
+
self.name = name # the name of the host (hostname or IP address)
|
|
450
|
+
self.command = command # the command to run on the host
|
|
451
|
+
self.returncode = None # the return code of the command
|
|
452
|
+
self.output = [] # the output of the command for curses
|
|
453
|
+
self.stdout = [] # the stdout of the command
|
|
454
|
+
self.stderr = [] # the stderr of the command
|
|
455
|
+
self.lineNumToPrintSet = set() # line numbers to reprint
|
|
456
|
+
self.lastUpdateTime = time.monotonic() # the last time the output was updated
|
|
457
|
+
self.lastPrintedUpdateTime = 0 # the last time the output was printed
|
|
458
|
+
self.files = files # the files to be copied to the host
|
|
459
|
+
self.ipmi = ipmi # whether to use ipmi to connect to the host
|
|
460
|
+
self.shell = shell # whether to use shell to run the command
|
|
461
|
+
self.interface_ip_prefix = interface_ip_prefix # the prefix of the ip address of the interface to be used to connect to the host
|
|
462
|
+
self.scp = scp # whether to use scp to copy files to the host
|
|
463
|
+
self.gatherMode = gatherMode # whether the host is in gather mode
|
|
464
|
+
self.extraargs = extraargs # extra arguments to be passed to ssh
|
|
465
|
+
self.resolvedName = None # the resolved IP address of the host
|
|
466
|
+
# also store a globally unique integer i from 0
|
|
467
|
+
self.i = i if i != -1 else _get_i()
|
|
468
|
+
self.uuid = uuid
|
|
469
|
+
self.identity_file = identity_file
|
|
470
|
+
self.ip = ip if ip else getIP(name)
|
|
471
|
+
self.current_color_pair = [-1, -1, 1]
|
|
472
|
+
self.output_buffer = io.BytesIO()
|
|
473
|
+
self.stdout_buffer = io.BytesIO()
|
|
474
|
+
self.stderr_buffer = io.BytesIO()
|
|
475
|
+
self.thread = None
|
|
476
|
+
|
|
477
|
+
def __iter__(self):
|
|
478
|
+
return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
|
|
479
|
+
def __repr__(self):
|
|
480
|
+
return f"Host(\n{pformat(self.__dict__)}\n)"
|
|
481
|
+
def __str__(self):
|
|
482
|
+
return f"Host(\n{pformat(dict(self))}\n)"
|
|
483
|
+
def get_output_hash(self):
|
|
484
|
+
return hash((
|
|
485
|
+
self.command,
|
|
486
|
+
tuple(self.stdout),
|
|
487
|
+
tuple(self.stderr),
|
|
488
|
+
self.returncode
|
|
489
|
+
))
|
|
490
|
+
def copy(self):
|
|
491
|
+
newHost = Host(
|
|
492
|
+
name=self.name,
|
|
493
|
+
command=self.command,
|
|
494
|
+
files=self.files,
|
|
495
|
+
ipmi=self.ipmi,
|
|
496
|
+
interface_ip_prefix=self.interface_ip_prefix,
|
|
497
|
+
scp=self.scp,
|
|
498
|
+
extraargs=self.extraargs,
|
|
499
|
+
gatherMode=self.gatherMode,
|
|
500
|
+
identity_file=self.identity_file,
|
|
501
|
+
shell=self.shell,
|
|
502
|
+
i=self.i,
|
|
503
|
+
uuid=self.uuid,
|
|
504
|
+
ip=self.ip
|
|
505
|
+
)
|
|
506
|
+
newHost.returncode = self.returncode
|
|
507
|
+
newHost.output = self.output.copy()
|
|
508
|
+
newHost.stdout = self.stdout.copy()
|
|
509
|
+
newHost.stderr = self.stderr.copy()
|
|
510
|
+
newHost.lineNumToPrintSet = self.lineNumToPrintSet.copy()
|
|
511
|
+
newHost.lastUpdateTime = self.lastUpdateTime
|
|
512
|
+
newHost.lastPrintedUpdateTime = self.lastPrintedUpdateTime
|
|
513
|
+
newHost.current_color_pair = self.current_color_pair.copy()
|
|
514
|
+
return newHost
|
|
516
515
|
|
|
516
|
+
#%% ------------ Exportable Help Functions ----------------
|
|
517
517
|
def find_ssh_key_file(searchPath = DEFAULT_SSH_KEY_SEARCH_PATH):
|
|
518
518
|
'''
|
|
519
519
|
Find the ssh public key file
|
|
@@ -528,8 +528,7 @@ def find_ssh_key_file(searchPath = DEFAULT_SSH_KEY_SEARCH_PATH):
|
|
|
528
528
|
sshKeyPath = searchPath
|
|
529
529
|
else:
|
|
530
530
|
sshKeyPath ='~/.ssh'
|
|
531
|
-
|
|
532
|
-
for sshKeyFile in possibleSshKeyFiles:
|
|
531
|
+
for sshKeyFile in POSSIBLE_SSH_KEY_FILES:
|
|
533
532
|
if os.path.exists(os.path.expanduser(os.path.join(sshKeyPath,sshKeyFile))):
|
|
534
533
|
return os.path.join(sshKeyPath,sshKeyFile)
|
|
535
534
|
return None
|
|
@@ -3357,7 +3356,6 @@ def processRunOnHosts(timeout, password, max_connections, hosts, return_unfinish
|
|
|
3357
3356
|
|
|
3358
3357
|
|
|
3359
3358
|
#%% ------------ Stringfy Block ----------------
|
|
3360
|
-
|
|
3361
3359
|
def formHostStr(host) -> str:
|
|
3362
3360
|
"""
|
|
3363
3361
|
Forms a comma-separated string of hosts.
|
|
@@ -3896,6 +3894,7 @@ def generate_default_config(args):
|
|
|
3896
3894
|
'SSH_STRICT_HOST_KEY_CHECKING': SSH_STRICT_HOST_KEY_CHECKING,
|
|
3897
3895
|
'ERROR_MESSAGES_TO_IGNORE': ERROR_MESSAGES_TO_IGNORE,
|
|
3898
3896
|
'FORCE_TRUECOLOR': args.force_truecolor,
|
|
3897
|
+
'POSSIBLE_SSH_KEY_FILES': POSSIBLE_SSH_KEY_FILES,
|
|
3899
3898
|
}
|
|
3900
3899
|
|
|
3901
3900
|
def write_default_config(args,CONFIG_FILE = None, force = False):
|
|
@@ -3939,9 +3938,17 @@ def write_default_config(args,CONFIG_FILE = None, force = False):
|
|
|
3939
3938
|
|
|
3940
3939
|
#%% ------------ Argument Processing -----------------
|
|
3941
3940
|
def get_parser():
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3941
|
+
parser = argparse.ArgumentParser(
|
|
3942
|
+
description='Run a command on multiple hosts, Use #HOST# or #HOSTNAME# to replace the host name in the command.',
|
|
3943
|
+
epilog=(
|
|
3944
|
+
f"Terminal color capability: {get_terminal_color_capability()}\n"
|
|
3945
|
+
f"Found bins: \n{pformat(set(_binPaths.values()))}\n"
|
|
3946
|
+
f"Missing bins: \n{pformat(_binCalled - set(_binPaths.keys()))}\n"
|
|
3947
|
+
f"Config file chain: \n{pformat(CONFIG_FILE_CHAIN)}\n"
|
|
3948
|
+
f"Hosts Env files searched:\n{pformat(DEFAULT_ENV_FILES)}"
|
|
3949
|
+
),
|
|
3950
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
3951
|
+
)
|
|
3945
3952
|
parser.add_argument('hosts', metavar='hosts', type=str, nargs='?', help=f'Hosts to run the command on, use "," to seperate hosts. (default: {DEFAULT_HOSTS})',default=DEFAULT_HOSTS)
|
|
3946
3953
|
parser.add_argument('commands', metavar='commands', type=str, nargs='*',default=None,help='the command to run on the hosts / the destination of the files #HOST# or #HOSTNAME# will be replaced with the host name.')
|
|
3947
3954
|
parser.add_argument('-u','--username', type=str,help=f'The general username to use to connect to the hosts. Will get overwrote by individual username@host if specified. (default: {DEFAULT_USERNAME})',default=DEFAULT_USERNAME)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|