multiSSH3 5.25__py3-none-any.whl → 5.30__py3-none-any.whl
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.
Potentially problematic release.
This version of multiSSH3 might be problematic. Click here for more details.
- {multiSSH3-5.25.dist-info → multiSSH3-5.30.dist-info}/METADATA +18 -10
- multiSSH3-5.30.dist-info/RECORD +7 -0
- {multiSSH3-5.25.dist-info → multiSSH3-5.30.dist-info}/WHEEL +1 -1
- multiSSH3.py +371 -59
- multiSSH3-5.25.dist-info/RECORD +0 -7
- {multiSSH3-5.25.dist-info → multiSSH3-5.30.dist-info}/LICENSE +0 -0
- {multiSSH3-5.25.dist-info → multiSSH3-5.30.dist-info}/entry_points.txt +0 -0
- {multiSSH3-5.25.dist-info → multiSSH3-5.30.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: multiSSH3
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.30
|
|
4
4
|
Summary: Run commands on multiple hosts via SSH
|
|
5
5
|
Home-page: https://github.com/yufei-pan/multiSSH3
|
|
6
6
|
Author: Yufei Pan
|
|
@@ -96,7 +96,7 @@ An example .ssh/config:
|
|
|
96
96
|
Host *
|
|
97
97
|
StrictHostKeyChecking no
|
|
98
98
|
ControlMaster auto
|
|
99
|
-
ControlPath /
|
|
99
|
+
ControlPath /tmp/%u_ssh_sockets_%r@%h-%p
|
|
100
100
|
ControlPersist 3600
|
|
101
101
|
```
|
|
102
102
|
|
|
@@ -124,10 +124,10 @@ While leaving minimum 40 characters / 1 line for each host display by default. Y
|
|
|
124
124
|
Use ```mssh --help``` for more info.
|
|
125
125
|
|
|
126
126
|
```bash
|
|
127
|
-
usage: mssh [-h] [-u USERNAME] [-p PASSWORD] [-ea EXTRAARGS] [-11] [-f FILE] [-fs] [--scp] [-gm] [-t TIMEOUT] [-r REPEAT]
|
|
128
|
-
[--ipmi] [-mpre IPMI_INTERFACE_IP_PREFIX] [-pre INTERFACE_IP_PREFIX] [-q] [-ww WINDOW_WIDTH] [-wh WINDOW_HEIGHT]
|
|
129
|
-
[-no] [--no_env] [--env_file ENV_FILE] [-m MAX_CONNECTIONS] [-j] [--success_hosts] [-g] [-su
|
|
130
|
-
[--store_config_file] [--debug] [
|
|
127
|
+
usage: mssh [-h] [-u USERNAME] [-p PASSWORD] [-k [KEY]] [-uk] [-ea EXTRAARGS] [-11] [-f FILE] [-fs] [--scp] [-gm] [-t TIMEOUT] [-r REPEAT]
|
|
128
|
+
[-i INTERVAL] [--ipmi] [-mpre IPMI_INTERFACE_IP_PREFIX] [-pre INTERFACE_IP_PREFIX] [-q] [-ww WINDOW_WIDTH] [-wh WINDOW_HEIGHT]
|
|
129
|
+
[-sw] [-eo] [-no] [--no_env] [--env_file ENV_FILE] [-m MAX_CONNECTIONS] [-j] [--success_hosts] [-g] [-su | -nsu]
|
|
130
|
+
[-sh SKIP_HOSTS] [--store_config_file] [--debug] [-ci] [-V]
|
|
131
131
|
[hosts] [commands ...]
|
|
132
132
|
|
|
133
133
|
Run a command on multiple hosts, Use #HOST# or #HOSTNAME# to replace the host name in the command. Config file: /etc/multiSSH3.config.json
|
|
@@ -144,6 +144,10 @@ options:
|
|
|
144
144
|
(default: None)
|
|
145
145
|
-p PASSWORD, --password PASSWORD
|
|
146
146
|
The password to use to connect to the hosts, (default: )
|
|
147
|
+
-k [KEY], --key [KEY], --identity [KEY]
|
|
148
|
+
The identity file to use to connect to the hosts. Implies --use_key. Specify a folder for program to search for a
|
|
149
|
+
key. Use option without value to use ~/.ssh/ (default: None)
|
|
150
|
+
-uk, --use_key Attempt to use public key file to connect to the hosts. (default: False)
|
|
147
151
|
-ea EXTRAARGS, --extraargs EXTRAARGS
|
|
148
152
|
Extra arguments to pass to the ssh / rsync / scp command. Put in one string for multiple arguments.Use "=" ! Ex.
|
|
149
153
|
-ea="--delete" (default: None)
|
|
@@ -181,16 +185,20 @@ options:
|
|
|
181
185
|
Max number of connections to use (default: 4 * cpu_count)
|
|
182
186
|
-j, --json Output in json format. (default: False)
|
|
183
187
|
--success_hosts Output the hosts that succeeded in summary as wells. (default: False)
|
|
184
|
-
-g, --greppable
|
|
188
|
+
-g, --greppable, --table
|
|
189
|
+
Output in greppable format. (default: False)
|
|
185
190
|
-su, --skip_unreachable
|
|
186
|
-
Skip unreachable hosts
|
|
187
|
-
|
|
191
|
+
Skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will
|
|
192
|
+
still auto skip unreachable hosts. (default: False)
|
|
193
|
+
-nsu, --no_skip_unreachable
|
|
194
|
+
Do not skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence
|
|
195
|
+
will still auto skip unreachable hosts. (default: True)
|
|
188
196
|
-sh SKIP_HOSTS, --skip_hosts SKIP_HOSTS
|
|
189
197
|
Skip the hosts in the list. (default: None)
|
|
190
198
|
--store_config_file Store / generate the default config file from command line argument and current config at
|
|
191
199
|
/etc/multiSSH3.config.json
|
|
192
200
|
--debug Print debug information
|
|
193
|
-
--
|
|
201
|
+
-ci, --copy_id Copy the ssh id to the hosts
|
|
194
202
|
-V, --version show program's version number and exit
|
|
195
203
|
```
|
|
196
204
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
multiSSH3.py,sha256=fYk9xAyS1aEYuAlDsPXWCnfywu9DL4HzlsapwZXvw4I,130177
|
|
2
|
+
multiSSH3-5.30.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
3
|
+
multiSSH3-5.30.dist-info/METADATA,sha256=jSPXglJf5mXUNq4d5yfXocRc5pjYfdwXgkziKZGCC0w,18160
|
|
4
|
+
multiSSH3-5.30.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
5
|
+
multiSSH3-5.30.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
|
|
6
|
+
multiSSH3-5.30.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
|
|
7
|
+
multiSSH3-5.30.dist-info/RECORD,,
|
multiSSH3.py
CHANGED
|
@@ -24,6 +24,7 @@ import shutil
|
|
|
24
24
|
import getpass
|
|
25
25
|
import uuid
|
|
26
26
|
import tempfile
|
|
27
|
+
import math
|
|
27
28
|
|
|
28
29
|
try:
|
|
29
30
|
# Check if functiools.cache is available
|
|
@@ -36,7 +37,7 @@ except AttributeError:
|
|
|
36
37
|
# If neither is available, use a dummy decorator
|
|
37
38
|
def cache_decorator(func):
|
|
38
39
|
return func
|
|
39
|
-
version = '5.
|
|
40
|
+
version = '5.30'
|
|
40
41
|
VERSION = version
|
|
41
42
|
|
|
42
43
|
CONFIG_FILE = '/etc/multiSSH3.config.json'
|
|
@@ -179,12 +180,17 @@ class Host:
|
|
|
179
180
|
self.uuid = uuid
|
|
180
181
|
self.identity_file = identity_file
|
|
181
182
|
self.ip = ip if ip else getIP(name)
|
|
183
|
+
self.current_color_pair = [-1, -1, 1]
|
|
182
184
|
|
|
183
185
|
def __iter__(self):
|
|
184
186
|
return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
|
|
185
187
|
def __repr__(self):
|
|
186
188
|
# return the complete data structure
|
|
187
|
-
return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr},
|
|
189
|
+
return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr}, \
|
|
190
|
+
output={self.output}, printedLines={self.printedLines}, files={self.files}, ipmi={self.ipmi}, \
|
|
191
|
+
interface_ip_prefix={self.interface_ip_prefix}, scp={self.scp}, gatherMode={self.gatherMode}, \
|
|
192
|
+
extraargs={self.extraargs}, resolvedName={self.resolvedName}, i={self.i}, uuid={self.uuid}), \
|
|
193
|
+
identity_file={self.identity_file}, ip={self.ip}, current_color_pair={self.current_color_pair}"
|
|
188
194
|
def __str__(self):
|
|
189
195
|
return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr})"
|
|
190
196
|
|
|
@@ -341,7 +347,30 @@ if True:
|
|
|
341
347
|
__keyPressesIn = [[]]
|
|
342
348
|
_emo = False
|
|
343
349
|
_etc_hosts = __configs_from_file.get('_etc_hosts', __build_in_default_config['_etc_hosts'])
|
|
344
|
-
|
|
350
|
+
__curses_global_color_pairs = {(-1,-1):1}
|
|
351
|
+
__curses_current_color_pair_index = 2 # Start from 1, as 0 is the default color pair
|
|
352
|
+
__curses_color_table = {}
|
|
353
|
+
__curses_current_color_index = 10
|
|
354
|
+
|
|
355
|
+
# Mapping of ANSI 4-bit colors to curses colors
|
|
356
|
+
ANSI_TO_CURSES_COLOR = {
|
|
357
|
+
30: curses.COLOR_BLACK,
|
|
358
|
+
31: curses.COLOR_RED,
|
|
359
|
+
32: curses.COLOR_GREEN,
|
|
360
|
+
33: curses.COLOR_YELLOW,
|
|
361
|
+
34: curses.COLOR_BLUE,
|
|
362
|
+
35: curses.COLOR_MAGENTA,
|
|
363
|
+
36: curses.COLOR_CYAN,
|
|
364
|
+
37: curses.COLOR_WHITE,
|
|
365
|
+
90: curses.COLOR_BLACK, # Bright Black (usually gray)
|
|
366
|
+
91: curses.COLOR_RED, # Bright Red
|
|
367
|
+
92: curses.COLOR_GREEN, # Bright Green
|
|
368
|
+
93: curses.COLOR_YELLOW, # Bright Yellow
|
|
369
|
+
94: curses.COLOR_BLUE, # Bright Blue
|
|
370
|
+
95: curses.COLOR_MAGENTA, # Bright Magenta
|
|
371
|
+
96: curses.COLOR_CYAN, # Bright Cyan
|
|
372
|
+
97: curses.COLOR_WHITE # Bright White
|
|
373
|
+
}
|
|
345
374
|
# ------------ Exportable Help Functions ----------------
|
|
346
375
|
# check if command sshpass is available
|
|
347
376
|
_binPaths = {}
|
|
@@ -758,7 +787,7 @@ def __filterSumDic(sumDic):
|
|
|
758
787
|
return newSumDic
|
|
759
788
|
|
|
760
789
|
@cache_decorator
|
|
761
|
-
def
|
|
790
|
+
def __compact_hostnames(Hostnames):
|
|
762
791
|
"""
|
|
763
792
|
Compact a list of hostnames.
|
|
764
793
|
Compact numeric numbers into ranges.
|
|
@@ -803,6 +832,43 @@ def compact_hostnames(Hostnames):
|
|
|
803
832
|
rtnSet.add(''.join(hostnameList))
|
|
804
833
|
return frozenset(rtnSet)
|
|
805
834
|
|
|
835
|
+
def compact_hostnames(Hostnames):
|
|
836
|
+
"""
|
|
837
|
+
Compact a list of hostnames.
|
|
838
|
+
Compact numeric numbers into ranges.
|
|
839
|
+
|
|
840
|
+
Args:
|
|
841
|
+
Hostnames (list): A list of hostnames.
|
|
842
|
+
|
|
843
|
+
Returns:
|
|
844
|
+
list: A list of comapcted hostname list.
|
|
845
|
+
|
|
846
|
+
Example:
|
|
847
|
+
>>> compact_hostnames(['server15', 'server16', 'server17'])
|
|
848
|
+
['server[15-17]']
|
|
849
|
+
>>> compact_hostnames(['server-1', 'server-2', 'server-3'])
|
|
850
|
+
['server-[1-3]']
|
|
851
|
+
>>> compact_hostnames(['server-1-2', 'server-1-1', 'server-2-1', 'server-2-2'])
|
|
852
|
+
['server-[1-2]-[1-2]']
|
|
853
|
+
>>> compact_hostnames(['server-1-2', 'server-1-1', 'server-2-2'])
|
|
854
|
+
['server-1-[1-2]', 'server-2-2']
|
|
855
|
+
>>> compact_hostnames(['test1-a', 'test2-a'])
|
|
856
|
+
['test[1-2]-a']
|
|
857
|
+
>>> compact_hostnames(['sub-s1', 'sub-s2'])
|
|
858
|
+
['sub-s[1-2]']
|
|
859
|
+
"""
|
|
860
|
+
global __global_suppress_printout
|
|
861
|
+
if not isinstance(Hostnames, frozenset):
|
|
862
|
+
hostSet = frozenset(Hostnames)
|
|
863
|
+
else:
|
|
864
|
+
hostSet = Hostnames
|
|
865
|
+
compact_hosts = __compact_hostnames(hostSet)
|
|
866
|
+
if set(expand_hostnames(compact_hosts)) != set(expand_hostnames(hostSet)):
|
|
867
|
+
if not __global_suppress_printout:
|
|
868
|
+
eprint(f"Error compacting hostnames: {hostSet} -> {compact_hosts}")
|
|
869
|
+
compact_hosts = hostSet
|
|
870
|
+
return compact_hosts
|
|
871
|
+
|
|
806
872
|
# ------------ Expanding Hostnames ----------------
|
|
807
873
|
@cache_decorator
|
|
808
874
|
def __validate_expand_hostname(hostname):
|
|
@@ -823,10 +889,10 @@ def __validate_expand_hostname(hostname):
|
|
|
823
889
|
return [hostname]
|
|
824
890
|
elif not _no_env and hostname in os.environ:
|
|
825
891
|
# we will expand these hostnames again
|
|
826
|
-
return expand_hostnames(
|
|
892
|
+
return expand_hostnames(os.environ[hostname].split(','))
|
|
827
893
|
elif hostname in readEnvFromFile():
|
|
828
894
|
# we will expand these hostnames again
|
|
829
|
-
return expand_hostnames(
|
|
895
|
+
return expand_hostnames(readEnvFromFile()[hostname].split(','))
|
|
830
896
|
elif getIP(hostname,local=False):
|
|
831
897
|
return [hostname]
|
|
832
898
|
else:
|
|
@@ -940,7 +1006,7 @@ def __expand_hostname(text, validate=True):# -> set:
|
|
|
940
1006
|
return expandedhosts
|
|
941
1007
|
|
|
942
1008
|
@cache_decorator
|
|
943
|
-
def
|
|
1009
|
+
def __expand_hostnames(hosts) -> dict:
|
|
944
1010
|
'''
|
|
945
1011
|
Expand the hostnames in the hosts into a dictionary
|
|
946
1012
|
|
|
@@ -951,8 +1017,6 @@ def expand_hostnames(hosts) -> dict:
|
|
|
951
1017
|
dict: A dictionary of expanded hostnames with key: hostname, value: resolved IP address
|
|
952
1018
|
'''
|
|
953
1019
|
expandedhosts = {}
|
|
954
|
-
if isinstance(hosts, str):
|
|
955
|
-
hosts = [hosts]
|
|
956
1020
|
for host in hosts:
|
|
957
1021
|
host = host.strip()
|
|
958
1022
|
if not host:
|
|
@@ -986,6 +1050,24 @@ def expand_hostnames(hosts) -> dict:
|
|
|
986
1050
|
[expandedhosts.update({host:ip}) for host,ip in zip(hostSetToAdd,iplist)]
|
|
987
1051
|
return expandedhosts
|
|
988
1052
|
|
|
1053
|
+
def expand_hostnames(hosts):
|
|
1054
|
+
'''
|
|
1055
|
+
Expand the hostnames in the hosts into a dictionary
|
|
1056
|
+
|
|
1057
|
+
Args:
|
|
1058
|
+
hosts (list): A list of hostnames
|
|
1059
|
+
|
|
1060
|
+
Returns:
|
|
1061
|
+
dict: A dictionary of expanded hostnames with key: hostname, value: resolved IP address
|
|
1062
|
+
'''
|
|
1063
|
+
if isinstance(hosts, str):
|
|
1064
|
+
hosts = [hosts]
|
|
1065
|
+
# change data type to frozenset if it is not hashable
|
|
1066
|
+
if not isinstance(hosts, frozenset):
|
|
1067
|
+
hosts = frozenset(hosts)
|
|
1068
|
+
return __expand_hostnames(hosts)
|
|
1069
|
+
|
|
1070
|
+
|
|
989
1071
|
# ------------ Run Command Block ----------------
|
|
990
1072
|
def __handle_reading_stream(stream,target, host):
|
|
991
1073
|
'''
|
|
@@ -1368,6 +1450,227 @@ def start_run_on_hosts(hosts, timeout=60,password=None,max_connections=4 * os.cp
|
|
|
1368
1450
|
return threads
|
|
1369
1451
|
|
|
1370
1452
|
# ------------ Display Block ----------------
|
|
1453
|
+
def __approximate_color_8bit(color):
|
|
1454
|
+
"""
|
|
1455
|
+
Approximate an 8-bit color (0-255) to the nearest curses color.
|
|
1456
|
+
|
|
1457
|
+
Args:
|
|
1458
|
+
color: 8-bit color code
|
|
1459
|
+
|
|
1460
|
+
Returns:
|
|
1461
|
+
Curses color code
|
|
1462
|
+
"""
|
|
1463
|
+
if color < 8: # Standard and bright colors
|
|
1464
|
+
return ANSI_TO_CURSES_COLOR.get(color % 8 + 30, curses.COLOR_WHITE)
|
|
1465
|
+
elif 8 <= color < 16: # Bright colors
|
|
1466
|
+
return ANSI_TO_CURSES_COLOR.get(color % 8 + 90, curses.COLOR_WHITE)
|
|
1467
|
+
elif 16 <= color <= 231: # Color cube
|
|
1468
|
+
# Convert 216-color cube index to RGB
|
|
1469
|
+
color -= 16
|
|
1470
|
+
r = (color // 36) % 6 * 51
|
|
1471
|
+
g = (color // 6) % 6 * 51
|
|
1472
|
+
b = color % 6 * 51
|
|
1473
|
+
return __approximate_color_24bit(r, g, b) # Map to the closest curses color
|
|
1474
|
+
elif 232 <= color <= 255: # Grayscale
|
|
1475
|
+
gray = (color - 232) * 10 + 8
|
|
1476
|
+
return __approximate_color_24bit(gray, gray, gray)
|
|
1477
|
+
else:
|
|
1478
|
+
return curses.COLOR_WHITE # Fallback to white for unexpected values
|
|
1479
|
+
|
|
1480
|
+
def __approximate_color_24bit(r, g, b):
|
|
1481
|
+
"""
|
|
1482
|
+
Approximate a 24-bit RGB color to the nearest curses color.
|
|
1483
|
+
Will initiate a curses color if curses.can_change_color() is True.
|
|
1484
|
+
|
|
1485
|
+
Globals:
|
|
1486
|
+
__curses_color_table: Dictionary of RGB color to curses color code
|
|
1487
|
+
__curses_current_color_index: Current index of the
|
|
1488
|
+
|
|
1489
|
+
Args:
|
|
1490
|
+
r: Red component (0-255)
|
|
1491
|
+
g: Green component (0-255)
|
|
1492
|
+
b: Blue component (0-255)
|
|
1493
|
+
|
|
1494
|
+
Returns:
|
|
1495
|
+
Curses color code
|
|
1496
|
+
"""
|
|
1497
|
+
if curses.can_change_color():
|
|
1498
|
+
global __curses_color_table,__curses_current_color_index
|
|
1499
|
+
# Initiate a new color if it does not exist
|
|
1500
|
+
if (r, g, b) not in __curses_color_table:
|
|
1501
|
+
if __curses_current_color_index >= curses.COLORS:
|
|
1502
|
+
eprint("Warning: Maximum number of colors reached. Wrapping around.")
|
|
1503
|
+
__curses_current_color_index = 10
|
|
1504
|
+
curses.init_color(__curses_current_color_index, int(r/255*1000), int(g/255*1000), int(b/255*1000))
|
|
1505
|
+
__curses_color_table[(r, g, b)] = __curses_current_color_index
|
|
1506
|
+
__curses_current_color_index += 1
|
|
1507
|
+
return __curses_color_table[(r, g, b)]
|
|
1508
|
+
# Fallback to 8-bit color approximation
|
|
1509
|
+
colors = {
|
|
1510
|
+
curses.COLOR_BLACK: (0, 0, 0),
|
|
1511
|
+
curses.COLOR_RED: (255, 0, 0),
|
|
1512
|
+
curses.COLOR_GREEN: (0, 255, 0),
|
|
1513
|
+
curses.COLOR_YELLOW: (255, 255, 0),
|
|
1514
|
+
curses.COLOR_BLUE: (0, 0, 255),
|
|
1515
|
+
curses.COLOR_MAGENTA: (255, 0, 255),
|
|
1516
|
+
curses.COLOR_CYAN: (0, 255, 255),
|
|
1517
|
+
curses.COLOR_WHITE: (255, 255, 255),
|
|
1518
|
+
}
|
|
1519
|
+
best_match = curses.COLOR_WHITE
|
|
1520
|
+
min_distance = float("inf")
|
|
1521
|
+
for color, (cr, cg, cb) in colors.items():
|
|
1522
|
+
distance = math.sqrt((r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2)
|
|
1523
|
+
if distance < min_distance:
|
|
1524
|
+
min_distance = distance
|
|
1525
|
+
best_match = color
|
|
1526
|
+
return best_match
|
|
1527
|
+
|
|
1528
|
+
def __parse_ansi_escape_sequence_to_curses_color(escape_code):
|
|
1529
|
+
"""
|
|
1530
|
+
Parse ANSI escape codes to extract foreground and background colors.
|
|
1531
|
+
|
|
1532
|
+
Args:
|
|
1533
|
+
escape_code: ANSI escape sequence for color
|
|
1534
|
+
|
|
1535
|
+
Returns:
|
|
1536
|
+
Tuple of (foreground, background) curses color pairs.
|
|
1537
|
+
If the escape code is a reset code, return (-1, -1).
|
|
1538
|
+
None values indicate that the color should not be changed.
|
|
1539
|
+
"""
|
|
1540
|
+
if not escape_code:
|
|
1541
|
+
return None, None
|
|
1542
|
+
color_match = re.match(r"\x1b\[(\d+)(?:;(\d+))?(?:;(\d+))?(?:;(\d+);(\d+);(\d+))?m", escape_code)
|
|
1543
|
+
if color_match:
|
|
1544
|
+
params = color_match.groups()
|
|
1545
|
+
if params[0] == "0" and not any(params[1:]): # Reset code
|
|
1546
|
+
return -1, -1
|
|
1547
|
+
if params[0] == "38" and params[1] == "5": # 8-bit foreground
|
|
1548
|
+
return __approximate_color_8bit(int(params[2])), None
|
|
1549
|
+
elif params[0] == "38" and params[1] == "2": # 24-bit foreground
|
|
1550
|
+
return __approximate_color_24bit(int(params[3]), int(params[4]), int(params[5])), None
|
|
1551
|
+
elif params[0] == "48" and params[1] == "5": # 8-bit background
|
|
1552
|
+
return None , __approximate_color_8bit(int(params[2]))
|
|
1553
|
+
elif params[0] == "48" and params[1] == "2": # 24-bit background
|
|
1554
|
+
return None, __approximate_color_24bit(int(params[3]), int(params[4]), int(params[5]))
|
|
1555
|
+
else:
|
|
1556
|
+
fg = None
|
|
1557
|
+
bg = None
|
|
1558
|
+
if params[0] and params[0].isdigit(): # 4-bit color
|
|
1559
|
+
fg = ANSI_TO_CURSES_COLOR.get(int(params[0]), curses.COLOR_WHITE)
|
|
1560
|
+
if params[1] and params[1].isdigit():
|
|
1561
|
+
bg = ANSI_TO_CURSES_COLOR.get(int(params[1]), curses.COLOR_BLACK)
|
|
1562
|
+
return fg, bg
|
|
1563
|
+
return None, None
|
|
1564
|
+
|
|
1565
|
+
def __get_curses_color_pair(fg, bg):
|
|
1566
|
+
"""
|
|
1567
|
+
Use curses color int values to create a curses color pair.
|
|
1568
|
+
|
|
1569
|
+
Globals:
|
|
1570
|
+
__curses_global_color_pairs: Dictionary of color pairs
|
|
1571
|
+
__curses_current_color_pair_index: Current index of the color pair
|
|
1572
|
+
|
|
1573
|
+
Args:
|
|
1574
|
+
fg: Foreground color code
|
|
1575
|
+
bg: Background color code
|
|
1576
|
+
|
|
1577
|
+
Returns:
|
|
1578
|
+
Curses color pair code
|
|
1579
|
+
"""
|
|
1580
|
+
global __curses_global_color_pairs, __curses_current_color_pair_index
|
|
1581
|
+
if (fg, bg) not in __curses_global_color_pairs:
|
|
1582
|
+
if __curses_current_color_pair_index >= curses.COLOR_PAIRS:
|
|
1583
|
+
print("Warning: Maximum number of color pairs reached, wrapping around.")
|
|
1584
|
+
__curses_current_color_pair_index = 1
|
|
1585
|
+
curses.init_pair(__curses_current_color_pair_index, fg, bg)
|
|
1586
|
+
__curses_global_color_pairs[(fg, bg)] = __curses_current_color_pair_index
|
|
1587
|
+
__curses_current_color_pair_index += 1
|
|
1588
|
+
return curses.color_pair(__curses_global_color_pairs[(fg, bg)])
|
|
1589
|
+
|
|
1590
|
+
def _curses_add_string_to_window(window, line, y = 0, x = 0, number_of_char_to_write = -1, color_pair_list = [-1,-1,1],fill_char=' ',parse_ascii_colors = True,centered = False,lead_str = '', trail_str = '',box_ansi_color = None):
|
|
1591
|
+
"""
|
|
1592
|
+
Add a string to a curses window with / without ANSI color escape sequences translated to curses color pairs.
|
|
1593
|
+
|
|
1594
|
+
Args:
|
|
1595
|
+
window: curses window object
|
|
1596
|
+
line: The line to add
|
|
1597
|
+
y: Line position in the window. Use -1 to scroll the window up 1 line and add the line at the bottom
|
|
1598
|
+
x: Column position in the window
|
|
1599
|
+
number_of_char_to_write: Number of characters to write. -1 for all remaining space in line, 0 for no characters, and a positive integer for a specific number of characters.
|
|
1600
|
+
color_pair_list: List of [foreground, background, color_pair] curses color pair values
|
|
1601
|
+
fill_char: Character to fill the remaining space in the line
|
|
1602
|
+
parse_ascii_colors: Parse ASCII color codes
|
|
1603
|
+
centered: Center the text in the window
|
|
1604
|
+
lead_str: Leading string to add to the line
|
|
1605
|
+
trail_str: Trailing string to add to the line
|
|
1606
|
+
|
|
1607
|
+
Returns:
|
|
1608
|
+
None
|
|
1609
|
+
"""
|
|
1610
|
+
if window.getmaxyx()[0] == 0 or window.getmaxyx()[1] == 0 or x >= window.getmaxyx()[1]:
|
|
1611
|
+
return
|
|
1612
|
+
if x < 0:
|
|
1613
|
+
x = window.getmaxyx()[1] + x
|
|
1614
|
+
if number_of_char_to_write == -1:
|
|
1615
|
+
numChar = window.getmaxyx()[1] - x -1
|
|
1616
|
+
elif number_of_char_to_write == 0:
|
|
1617
|
+
return
|
|
1618
|
+
elif number_of_char_to_write + x > window.getmaxyx()[1]:
|
|
1619
|
+
numChar = window.getmaxyx()[1] - x -1
|
|
1620
|
+
else:
|
|
1621
|
+
numChar = number_of_char_to_write
|
|
1622
|
+
if numChar < 0:
|
|
1623
|
+
return
|
|
1624
|
+
if y < 0 or y >= window.getmaxyx()[0]:
|
|
1625
|
+
window.move(0, 0)
|
|
1626
|
+
window.deleteln()
|
|
1627
|
+
y = window.getmaxyx()[0] - 1
|
|
1628
|
+
if parse_ascii_colors:
|
|
1629
|
+
segments = re.split(r"(\x1b\[[\d;]*m)", line) # Split line by ANSI escape codes
|
|
1630
|
+
else:
|
|
1631
|
+
segments = [line]
|
|
1632
|
+
charsWritten = 0
|
|
1633
|
+
boxFrontColor, boxBackColor = color_pair_list[0], color_pair_list[1]
|
|
1634
|
+
newBoxFrontColor, newBoxBackColor = __parse_ansi_escape_sequence_to_curses_color(box_ansi_color)
|
|
1635
|
+
if newBoxFrontColor:
|
|
1636
|
+
boxFrontColor = newBoxFrontColor
|
|
1637
|
+
if newBoxBackColor:
|
|
1638
|
+
boxBackColor = newBoxBackColor
|
|
1639
|
+
boxColorPair = __get_curses_color_pair(boxFrontColor, boxBackColor)
|
|
1640
|
+
# first add the lead_str
|
|
1641
|
+
window.addnstr(y, x, lead_str, numChar, boxColorPair)
|
|
1642
|
+
charsWritten = min(len(lead_str), numChar)
|
|
1643
|
+
# process centering
|
|
1644
|
+
if centered:
|
|
1645
|
+
fill_length = numChar - len(lead_str) - len(trail_str) - sum([len(segment) for segment in segments if not segment.startswith("\x1b[")])
|
|
1646
|
+
window.addnstr(y, x + charsWritten, fill_char * (fill_length // 2), numChar - charsWritten, boxColorPair)
|
|
1647
|
+
charsWritten += min(fill_length // 2, numChar - charsWritten)
|
|
1648
|
+
# add the segments
|
|
1649
|
+
for segment in segments:
|
|
1650
|
+
if parse_ascii_colors and segment.startswith("\x1b["):
|
|
1651
|
+
# Parse ANSI escape sequence
|
|
1652
|
+
newFrontColor, newBackColor = __parse_ansi_escape_sequence_to_curses_color(segment)
|
|
1653
|
+
if newFrontColor is not None:
|
|
1654
|
+
color_pair_list[0] = newFrontColor
|
|
1655
|
+
if newBackColor is not None:
|
|
1656
|
+
color_pair_list[1] = newBackColor
|
|
1657
|
+
color_pair_list[2] = __get_curses_color_pair(color_pair_list[0], color_pair_list[1])
|
|
1658
|
+
#window.addnstr(y, x + charsWritten, str(color_pair_list[2]), numChar - charsWritten, color_pair_list[2])
|
|
1659
|
+
#charsWritten += min(len(str(color_pair_list[2])), numChar - charsWritten)
|
|
1660
|
+
else:
|
|
1661
|
+
# Add text with current color
|
|
1662
|
+
if charsWritten < numChar:
|
|
1663
|
+
window.addnstr(y, x + charsWritten, segment, numChar - charsWritten, color_pair_list[2])
|
|
1664
|
+
charsWritten += min(len(segment), numChar - charsWritten)
|
|
1665
|
+
# if we have finished printing segments but we still have space, we will fill it with fill_char
|
|
1666
|
+
if charsWritten + len(trail_str) < numChar:
|
|
1667
|
+
fillStr = fill_char * (numChar - charsWritten - len(trail_str))
|
|
1668
|
+
#fillStr = f'{color_pair_list}'
|
|
1669
|
+
window.addnstr(y, x + charsWritten, fillStr + trail_str, numChar - charsWritten, boxColorPair)
|
|
1670
|
+
charsWritten += numChar - charsWritten
|
|
1671
|
+
else:
|
|
1672
|
+
window.addnstr(y, x + charsWritten, trail_str, numChar - charsWritten, boxColorPair)
|
|
1673
|
+
|
|
1371
1674
|
def _get_hosts_to_display (hosts, max_num_hosts, hosts_to_display = None):
|
|
1372
1675
|
'''
|
|
1373
1676
|
Generate a list for the hosts to be displayed on the screen. This is used to display as much relevant information as possible.
|
|
@@ -1463,6 +1766,7 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
|
|
|
1463
1766
|
stdscr.nodelay(True)
|
|
1464
1767
|
# we generate a stats window at the top of the screen
|
|
1465
1768
|
stat_window = curses.newwin(1, max_x, 0, 0)
|
|
1769
|
+
stat_window.leaveok(True)
|
|
1466
1770
|
# We create a window for each host
|
|
1467
1771
|
host_windows = []
|
|
1468
1772
|
for i, host in enumerate(hosts_to_display):
|
|
@@ -1473,13 +1777,18 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
|
|
|
1473
1777
|
#print(f"Creating a window at {y},{x}")
|
|
1474
1778
|
# We create the window
|
|
1475
1779
|
host_window = curses.newwin(host_window_height, host_window_width, y, x)
|
|
1780
|
+
host_window.idlok(True)
|
|
1781
|
+
host_window.scrollok(True)
|
|
1782
|
+
host_window.leaveok(True)
|
|
1476
1783
|
host_windows.append(host_window)
|
|
1477
1784
|
# If there is space left, we will draw the bottom border
|
|
1478
1785
|
bottom_border = None
|
|
1479
1786
|
if y + host_window_height < org_dim[0]:
|
|
1480
1787
|
bottom_border = curses.newwin(1, max_x, y + host_window_height, 0)
|
|
1788
|
+
bottom_border.leaveok(True)
|
|
1481
1789
|
#bottom_border.clear()
|
|
1482
|
-
bottom_border.
|
|
1790
|
+
#bottom_border.addnstr(0, 0, '-' * (max_x - 1), max_x - 1)
|
|
1791
|
+
_curses_add_string_to_window(window=bottom_border, y=0, line='-' * (max_x - 1),fill_char='-')
|
|
1483
1792
|
bottom_border.refresh()
|
|
1484
1793
|
while host_stats['running'] > 0 or host_stats['waiting'] > 0:
|
|
1485
1794
|
# Check for keypress
|
|
@@ -1581,11 +1890,13 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
|
|
|
1581
1890
|
# encodedLine = encodedLine[:curserPosition] + ' ' + encodedLine[curserPosition:]
|
|
1582
1891
|
stats = '┍'+ f"Send CMD: {encodedLine}"[:max_x - 2].center(max_x - 2, "━")
|
|
1583
1892
|
if bottom_border:
|
|
1584
|
-
|
|
1893
|
+
target_length = max_x - 2 + len('\x1b[33m\x1b[0m\x1b[31m\x1b[0m\x1b[32m\x1b[0m')
|
|
1894
|
+
bottom_stats = '└'+ f" Total: {len(hosts)} Running: \x1b[33m{host_stats['running']}\x1b[0m Failed: \x1b[31m{host_stats['failed']}\x1b[0m Finished: \x1b[32m{host_stats['finished']}\x1b[0m Waiting: {host_stats['waiting']} "[:target_length].center(target_length, "─")
|
|
1585
1895
|
if bottom_stats != old_bottom_stat:
|
|
1586
1896
|
old_bottom_stat = bottom_stats
|
|
1587
1897
|
#bottom_border.clear()
|
|
1588
|
-
bottom_border.
|
|
1898
|
+
#bottom_border.addnstr(0, 0, bottom_stats, max_x - 1)
|
|
1899
|
+
_curses_add_string_to_window(window=bottom_border, y=0, line=bottom_stats)
|
|
1589
1900
|
bottom_border.refresh()
|
|
1590
1901
|
if stats != old_stat or curserPosition != old_cursor_position:
|
|
1591
1902
|
old_stat = stats
|
|
@@ -1598,9 +1909,9 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
|
|
|
1598
1909
|
#stat_window.clear()
|
|
1599
1910
|
#stat_window.addstr(0, 0, stats)
|
|
1600
1911
|
# add the line with curser that inverses the color at the curser position
|
|
1601
|
-
stat_window.addstr(0, 0, stats[:curserPositionStats]
|
|
1602
|
-
stat_window.
|
|
1603
|
-
stat_window.
|
|
1912
|
+
stat_window.addstr(0, 0, stats[:curserPositionStats])
|
|
1913
|
+
stat_window.addch(0,curserPositionStats, stats[curserPositionStats], curses.A_REVERSE)
|
|
1914
|
+
stat_window.addnstr(0, curserPositionStats + 1, stats[curserPositionStats + 1:], max_x - 1 - curserPositionStats)
|
|
1604
1915
|
stat_window.refresh()
|
|
1605
1916
|
# set the maximum refresh rate to 100 Hz
|
|
1606
1917
|
if time.perf_counter() - last_refresh_time < 0.01:
|
|
@@ -1614,17 +1925,19 @@ def __generate_display(stdscr, hosts, lineToDisplay = -1,curserPosition = 0, min
|
|
|
1614
1925
|
#host_window.clear()
|
|
1615
1926
|
# we will try to center the name of the host with ┼ at the beginning and end and ─ in between
|
|
1616
1927
|
linePrintOut = f'┼{(host.name+":["+host.command+"]")[:host_window_width - 2].center(host_window_width - 1, "─")}'.replace('\n', ' ').replace('\r', ' ').strip()
|
|
1617
|
-
host_window.
|
|
1928
|
+
host_window.addnstr(0, 0, linePrintOut, host_window_width - 1)
|
|
1929
|
+
#_add_line_with_ascii_colors(window=host_window, y=0, x=0, line=linePrintOut, n=host_window_width - 1, color_pair_list = host.current_color_pair)
|
|
1618
1930
|
# we will display the latest outputs of the host as much as we can
|
|
1619
1931
|
for i, line in enumerate(host.output[-(host_window_height - 1):]):
|
|
1620
1932
|
# print(f"Printng a line at {i + 1} with length of {len('│'+line[:host_window_width - 1])}")
|
|
1621
1933
|
# time.sleep(10)
|
|
1622
|
-
linePrintOut = ('│'+line[:host_window_width - 2].replace('\n', ' ').replace('\r', ' ')).strip()
|
|
1623
|
-
host_window.
|
|
1934
|
+
linePrintOut = ('│'+line[:host_window_width - 2].replace('\n', ' ').replace('\r', ' ')).strip()
|
|
1935
|
+
#host_window.addnstr(i + 1, 0, linePrintOut, host_window_width - 1)
|
|
1936
|
+
_curses_add_string_to_window(window=host_window, y=i + 1, line=linePrintOut, color_pair_list=host.current_color_pair)
|
|
1624
1937
|
# we draw the rest of the available lines
|
|
1625
1938
|
for i in range(len(host.output), host_window_height - 1):
|
|
1626
1939
|
# print(f"Printng a line at {i + 1} with length of {len('│')}")
|
|
1627
|
-
host_window.
|
|
1940
|
+
host_window.addnstr(i + 1, 0, '│'.ljust(host_window_width - 1, ' '), host_window_width - 1)
|
|
1628
1941
|
host.printedLines = len(host.output)
|
|
1629
1942
|
host_window.refresh()
|
|
1630
1943
|
except Exception as e:
|
|
@@ -1654,33 +1967,39 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
|
|
|
1654
1967
|
'''
|
|
1655
1968
|
# We create all the windows we need
|
|
1656
1969
|
# We initialize the color pair
|
|
1657
|
-
curses.start_color()
|
|
1658
1970
|
curses.curs_set(0)
|
|
1659
|
-
curses.
|
|
1660
|
-
curses.
|
|
1661
|
-
curses.init_pair(
|
|
1662
|
-
curses.init_pair(4, curses.COLOR_GREEN, curses.COLOR_BLACK)
|
|
1663
|
-
curses.init_pair(5, curses.COLOR_YELLOW, curses.COLOR_BLACK)
|
|
1664
|
-
curses.init_pair(6, curses.COLOR_BLUE, curses.COLOR_BLACK)
|
|
1665
|
-
curses.init_pair(7, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
|
|
1666
|
-
curses.init_pair(8, curses.COLOR_CYAN, curses.COLOR_BLACK)
|
|
1667
|
-
curses.init_pair(9, curses.COLOR_WHITE, curses.COLOR_RED)
|
|
1668
|
-
curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_GREEN)
|
|
1669
|
-
curses.init_pair(11, curses.COLOR_WHITE, curses.COLOR_YELLOW)
|
|
1670
|
-
curses.init_pair(12, curses.COLOR_WHITE, curses.COLOR_BLUE)
|
|
1671
|
-
curses.init_pair(13, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
|
|
1672
|
-
curses.init_pair(14, curses.COLOR_WHITE, curses.COLOR_CYAN)
|
|
1673
|
-
curses.init_pair(15, curses.COLOR_BLACK, curses.COLOR_RED)
|
|
1674
|
-
curses.init_pair(16, curses.COLOR_BLACK, curses.COLOR_GREEN)
|
|
1675
|
-
curses.init_pair(17, curses.COLOR_BLACK, curses.COLOR_YELLOW)
|
|
1676
|
-
curses.init_pair(18, curses.COLOR_BLACK, curses.COLOR_BLUE)
|
|
1677
|
-
curses.init_pair(19, curses.COLOR_BLACK, curses.COLOR_MAGENTA)
|
|
1678
|
-
curses.init_pair(20, curses.COLOR_BLACK, curses.COLOR_CYAN)
|
|
1679
|
-
|
|
1971
|
+
curses.start_color()
|
|
1972
|
+
curses.use_default_colors()
|
|
1973
|
+
curses.init_pair(1, -1, -1)
|
|
1680
1974
|
# do not generate display if the output window have a size of zero
|
|
1681
1975
|
if stdscr.getmaxyx()[0] < 2 or stdscr.getmaxyx()[1] < 2:
|
|
1682
1976
|
return
|
|
1683
|
-
|
|
1977
|
+
stdscr.idlok(True)
|
|
1978
|
+
stdscr.scrollok(True)
|
|
1979
|
+
stdscr.leaveok(True)
|
|
1980
|
+
# generate some debug information before display initialization
|
|
1981
|
+
try:
|
|
1982
|
+
stdscr.clear()
|
|
1983
|
+
_curses_add_string_to_window(window=stdscr, y=0, line='Initializing display...', n=stdscr.getmaxyx()[1] - 1)
|
|
1984
|
+
# print the size
|
|
1985
|
+
_curses_add_string_to_window(window=stdscr, y=1, line=f"Terminal size: {stdscr.getmaxyx()}", n=stdscr.getmaxyx()[1] - 1)
|
|
1986
|
+
# print the number of hosts
|
|
1987
|
+
_curses_add_string_to_window(window=stdscr, y=2, line=f"Number of hosts: {len(hosts)}", n=stdscr.getmaxyx()[1] - 1)
|
|
1988
|
+
# print the number of threads
|
|
1989
|
+
_curses_add_string_to_window(window=stdscr, y=3, line=f"Number of threads: {len(threads)}", n=stdscr.getmaxyx()[1] - 1)
|
|
1990
|
+
# print the minimum character length
|
|
1991
|
+
_curses_add_string_to_window(window=stdscr, y=4, line=f"Minimum character length: {min_char_len}", n=stdscr.getmaxyx()[1] - 1)
|
|
1992
|
+
# print the minimum line length
|
|
1993
|
+
_curses_add_string_to_window(window=stdscr, y=5, line=f"Minimum line length: {min_line_len}", n=stdscr.getmaxyx()[1] - 1)
|
|
1994
|
+
# print the single window mode
|
|
1995
|
+
_curses_add_string_to_window(window=stdscr, y=6, line=f"Single window mode: {single_window}", n=stdscr.getmaxyx()[1] - 1)
|
|
1996
|
+
# print COLORS and COLOR_PAIRS count
|
|
1997
|
+
_curses_add_string_to_window(window=stdscr, y=7, line=f"len(COLORS): {curses.COLORS} len(COLOR_PAIRS): {curses.COLOR_PAIRS}", n=stdscr.getmaxyx()[1] - 1)
|
|
1998
|
+
# print if can change color
|
|
1999
|
+
_curses_add_string_to_window(window=stdscr, y=8, line=f"Real color capability: {curses.can_change_color()}", n=stdscr.getmaxyx()[1] - 1)
|
|
2000
|
+
stdscr.refresh()
|
|
2001
|
+
except:
|
|
2002
|
+
pass
|
|
1684
2003
|
params = (-1,0 , min_char_len, min_line_len, single_window,'new config')
|
|
1685
2004
|
while params:
|
|
1686
2005
|
params = __generate_display(stdscr, hosts, *params)
|
|
@@ -1763,17 +2082,13 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
|
|
|
1763
2082
|
outputs.setdefault(hostPrintOut, set()).add(host['name'])
|
|
1764
2083
|
rtnStr = ''
|
|
1765
2084
|
for output, hostSet in outputs.items():
|
|
1766
|
-
|
|
1767
|
-
compact_hosts = compact_hostnames(hostSet)
|
|
1768
|
-
if set(expand_hostnames(compact_hosts)) != set(expand_hostnames(hostSet)):
|
|
1769
|
-
eprint(f"Error compacting hostnames: {hostSet} -> {compact_hosts}")
|
|
1770
|
-
compact_hosts = hostSet
|
|
2085
|
+
compact_hosts = sorted(compact_hostnames(hostSet))
|
|
1771
2086
|
if __global_suppress_printout:
|
|
1772
2087
|
rtnStr += f'Abnormal returncode produced by {",".join(compact_hosts)}:\n'
|
|
1773
2088
|
rtnStr += output+'\n'
|
|
1774
2089
|
else:
|
|
1775
2090
|
rtnStr += '*'*80+'\n'
|
|
1776
|
-
rtnStr += f'These hosts: "{",".join(
|
|
2091
|
+
rtnStr += f'These hosts: "{",".join(compact_hosts)}" have a response of:\n'
|
|
1777
2092
|
rtnStr += output+'\n'
|
|
1778
2093
|
if not __global_suppress_printout or outputs:
|
|
1779
2094
|
rtnStr += '*'*80+'\n'
|
|
@@ -2065,13 +2380,13 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
|
|
|
2065
2380
|
if '@' not in host:
|
|
2066
2381
|
skipHostStr[i] = userStr + host
|
|
2067
2382
|
skipHostStr = ','.join(skipHostStr)
|
|
2068
|
-
targetHostDic = expand_hostnames(
|
|
2383
|
+
targetHostDic = expand_hostnames(hostStr.split(','))
|
|
2069
2384
|
if __DEBUG_MODE:
|
|
2070
2385
|
eprint(f"Target hosts: {targetHostDic!r}")
|
|
2071
|
-
skipHostsDic = expand_hostnames(
|
|
2072
|
-
if skipHostsDic:
|
|
2073
|
-
eprint(f"Skipping hosts: {skipHostsDic!r}")
|
|
2386
|
+
skipHostsDic = expand_hostnames(skipHostStr.split(','))
|
|
2074
2387
|
skipHostSet = set(skipHostsDic).union(skipHostsDic.values())
|
|
2388
|
+
if skipHostSet:
|
|
2389
|
+
eprint(f"Skipping hosts: \"{' '.join(sorted(compact_hostnames(skipHostSet)))}\"")
|
|
2075
2390
|
if copy_id:
|
|
2076
2391
|
if 'ssh-copy-id' in _binPaths:
|
|
2077
2392
|
# we will copy the id to the hosts
|
|
@@ -2421,18 +2736,15 @@ def main():
|
|
|
2421
2736
|
succeededHosts.add(host.name)
|
|
2422
2737
|
succeededHosts -= __failedHosts
|
|
2423
2738
|
# sort the failed hosts and succeeded hosts
|
|
2424
|
-
__failedHosts = sorted(__failedHosts)
|
|
2425
|
-
succeededHosts = sorted(succeededHosts)
|
|
2426
2739
|
if __mainReturnCode > 0:
|
|
2427
|
-
if not __global_suppress_printout:
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
if not __global_suppress_printout: eprint(f'failed_hosts: {",".join(__failedHosts)}')
|
|
2740
|
+
if not __global_suppress_printout:
|
|
2741
|
+
eprint(f'Complete. Failed hosts (Return Code not 0) count: {__mainReturnCode}')
|
|
2742
|
+
eprint(f'failed_hosts: {",".join(sorted(compact_hostnames(__failedHosts)))}')
|
|
2431
2743
|
else:
|
|
2432
2744
|
if not __global_suppress_printout: eprint('Complete. All hosts returned 0.')
|
|
2433
2745
|
|
|
2434
2746
|
if args.success_hosts and not __global_suppress_printout:
|
|
2435
|
-
eprint(f'succeeded_hosts: {",".join(succeededHosts)}')
|
|
2747
|
+
eprint(f'succeeded_hosts: {",".join(sorted(compact_hostnames(succeededHosts)))}')
|
|
2436
2748
|
|
|
2437
2749
|
if threading.active_count() > 1:
|
|
2438
2750
|
if not __global_suppress_printout: eprint(f'Remaining active thread: {threading.active_count()}')
|
multiSSH3-5.25.dist-info/RECORD
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
multiSSH3.py,sha256=LYOYALmpZm9TNmU2p-PtOYtZr7iruTrmEGfmjpHTJBA,118034
|
|
2
|
-
multiSSH3-5.25.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
3
|
-
multiSSH3-5.25.dist-info/METADATA,sha256=-Z06xUIxVOfhBIUyvTDtWKjxlay9RX__ni77zK9fWfQ,17517
|
|
4
|
-
multiSSH3-5.25.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
|
|
5
|
-
multiSSH3-5.25.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
|
|
6
|
-
multiSSH3-5.25.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
|
|
7
|
-
multiSSH3-5.25.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|