multiSSH3 4.98__py3-none-any.whl → 5.0__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: multiSSH3
3
- Version: 4.98
3
+ Version: 5.0
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
@@ -38,7 +38,7 @@ multissh will read a config file located at ```/etc/multiSSH3.config.json```
38
38
  To store / generate a config file with the current command line options, you can use
39
39
 
40
40
  ```bash
41
- mssh --generate_default_config_file
41
+ mssh --store_config_file
42
42
  ```
43
43
 
44
44
  You can modify the json file directly after generation and multissh will read from it for loading defaults.
@@ -47,10 +47,14 @@ Note:
47
47
 
48
48
  If you want to store password, it will be a plain text password in this config file. This will be better to supply it everytime as a CLI argument but you should really consider setting up priv-pub key setup.
49
49
 
50
+ Also Note:
51
+
52
+ On some systems, scp / rsync will require you use a priv-pub key to work
53
+
50
54
  This option can also be used to store cli options into the config files. For example.
51
55
 
52
56
  ```bash
53
- mssh --ipmi_interface_ip_prefix 192 --generate_default_config_file
57
+ mssh --ipmi_interface_ip_prefix 192 --store_config_file
54
58
  ```
55
59
  will store
56
60
  ```json
@@ -64,6 +68,22 @@ DEFAULT_ENV_FILE = '/etc/profile.d/hosts.sh'
64
68
  ```
65
69
  as hostname aliases.
66
70
 
71
+ multissh3 will resolve hostname grouping by:
72
+ ipv4 address expansion > local hostname resolution ( like /etc/hosts ) > currrent terminal environment > env from env_file > remote hostname resolution ( socket.gethostbyname() )
73
+
74
+ An example hostname alias file will look like:
75
+ ```bash
76
+ us_east='100.100.0.1-3,us_east_prod_[1-5]'
77
+ us_central=""
78
+ us_west="100.101.0.1-2,us_west_prod_[a-c]_[1-3]"
79
+ us="$us_east,$us_central,$us_west"
80
+ asia="100.90.0-1,1-9"
81
+ eu=''
82
+ rhel8="$asia,$us_east"
83
+ all="$us,$asia,$eu"
84
+ ```
85
+ ( You can use bash replacements for grouping. )
86
+
67
87
  For example:
68
88
  ```bash
69
89
  export all='192.168.1-2.1-64'
@@ -104,61 +124,73 @@ While leaving minimum 40 characters / 1 line for each host display by default. Y
104
124
  Use ```mssh --help``` for more info.
105
125
 
106
126
  ```bash
107
- usage: mssh [-h] [-u USERNAME] [-ea EXTRAARGS] [-p PASSWORD] [-11] [-f FILE] [--file_sync] [--scp] [-t TIMEOUT] [-r REPEAT] [-i INTERVAL] [--ipmi]
108
- [-pre INTERFACE_IP_PREFIX] [-q] [-ww WINDOW_WIDTH] [-wh WINDOW_HEIGHT] [-sw] [-eo] [-no] [--no_env] [--env_file ENV_FILE] [-m MAXCONNECTIONS] [-j]
109
- [--success_hosts] [-g] [-nw] [-su] [-sh SKIPHOSTS] [-V]
110
- hosts commands [commands ...]
127
+ usage: mssh [-h] [-u USERNAME] [-p PASSWORD] [-ea EXTRAARGS] [-11] [-f FILE] [-fs] [--scp] [-gm] [-t TIMEOUT] [-r REPEAT] [-i INTERVAL]
128
+ [--ipmi] [-mpre IPMI_INTERFACE_IP_PREFIX] [-pre INTERFACE_IP_PREFIX] [-q] [-ww WINDOW_WIDTH] [-wh WINDOW_HEIGHT] [-sw] [-eo]
129
+ [-no] [--no_env] [--env_file ENV_FILE] [-m MAX_CONNECTIONS] [-j] [--success_hosts] [-g] [-su] [-sh SKIP_HOSTS]
130
+ [--store_config_file] [--debug] [--copy-id] [-V]
131
+ [hosts] [commands ...]
111
132
 
112
- Run a command on multiple hosts, Use #HOST# or #HOSTNAME# to replace the host name in the command
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
113
134
 
114
135
  positional arguments:
115
- hosts Hosts to run the command on, use "," to seperate hosts
116
- commands the command to run on the hosts / the destination of the files #HOST# or #HOSTNAME# will be replaced with the host name.
136
+ hosts Hosts to run the command on, use "," to seperate hosts. (default: all)
137
+ commands the command to run on the hosts / the destination of the files #HOST# or #HOSTNAME# will be replaced with the host
138
+ name.
117
139
 
118
140
  options:
119
141
  -h, --help show this help message and exit
120
142
  -u USERNAME, --username USERNAME
121
- The general username to use to connect to the hosts. Will get overwrote by individual username@host if specified. (default: None)
122
- -ea EXTRAARGS, --extraargs EXTRAARGS
123
- Extra arguments to pass to the ssh / rsync / scp command. Put in one string for multiple arguments.Use "=" ! Ex. -ea="--delete" (default:
124
- None)
143
+ The general username to use to connect to the hosts. Will get overwrote by individual username@host if specified.
144
+ (default: None)
125
145
  -p PASSWORD, --password PASSWORD
126
- The password to use to connect to the hosts, (default: hermes)
146
+ The password to use to connect to the hosts, (default: )
147
+ -ea EXTRAARGS, --extraargs EXTRAARGS
148
+ Extra arguments to pass to the ssh / rsync / scp command. Put in one string for multiple arguments.Use "=" ! Ex.
149
+ -ea="--delete" (default: None)
127
150
  -11, --oneonone Run one corresponding command on each host. (default: False)
128
151
  -f FILE, --file FILE The file to be copied to the hosts. Use -f multiple times to copy multiple files
129
- --file_sync Operate in file sync mode, sync path in <COMMANDS> from this machine to <HOSTS>. Treat --file <FILE> and <COMMANDS> both as source as source
130
- and destination will be the same in this mode. (default: False)
152
+ -fs, --file_sync Operate in file sync mode, sync path in <COMMANDS> from this machine to <HOSTS>. Treat --file <FILE> and
153
+ <COMMANDS> both as source as source and destination will be the same in this mode. (default: False)
131
154
  --scp Use scp for copying files instead of rsync. Need to use this on windows. (default: False)
155
+ -gm, --gather_mode Gather files from the hosts instead of sending files to the hosts. Will send remote files specified in <FILE> to
156
+ local path specified in <COMMANDS> (default: False)
132
157
  -t TIMEOUT, --timeout TIMEOUT
133
- Timeout for each command in seconds (default: 0 (disabled))
158
+ Timeout for each command in seconds (default: 600 (disabled))
134
159
  -r REPEAT, --repeat REPEAT
135
160
  Repeat the command for a number of times (default: 1)
136
161
  -i INTERVAL, --interval INTERVAL
137
162
  Interval between repeats in seconds (default: 0)
138
163
  --ipmi Use ipmitool to run the command. (default: False)
164
+ -mpre IPMI_INTERFACE_IP_PREFIX, --ipmi_interface_ip_prefix IPMI_INTERFACE_IP_PREFIX
165
+ The prefix of the IPMI interfaces (default: )
139
166
  -pre INTERFACE_IP_PREFIX, --interface_ip_prefix INTERFACE_IP_PREFIX
140
167
  The prefix of the for the interfaces (default: None)
141
- -q, --quiet Quiet mode, no curses, only print the output. (default: False)
168
+ -q, -nw, --nowatch, --quiet
169
+ Quiet mode, no curses watch, only print the output. (default: False)
142
170
  -ww WINDOW_WIDTH, --window_width WINDOW_WIDTH
143
171
  The minimum character length of the curses window. (default: 40)
144
172
  -wh WINDOW_HEIGHT, --window_height WINDOW_HEIGHT
145
- The minimum line height of the curses window. (default: 1)
173
+ The minimum line height of the curses window. (default: 5)
146
174
  -sw, --single_window Use a single window for all hosts. (default: False)
147
175
  -eo, --error_only Only print the error output. (default: False)
148
- -no, --nooutput Do not print the output. (default: False)
149
- --no_env Do not load the environment variables. (default: False)
150
- --env_file ENV_FILE The file to load the environment variables from. (default: /etc/profile.d/hosts.sh)
151
- -m MAXCONNECTIONS, --maxconnections MAXCONNECTIONS
176
+ -no, --no_output Do not print the output. (default: False)
177
+ --no_env Do not load the command line environment variables. (default: False)
178
+ --env_file ENV_FILE The file to load the mssh file based environment variables from. ( Still work with --no_env ) (default:
179
+ /etc/profile.d/hosts.sh)
180
+ -m MAX_CONNECTIONS, --max_connections MAX_CONNECTIONS
152
181
  Max number of connections to use (default: 4 * cpu_count)
153
182
  -j, --json Output in json format. (default: False)
154
183
  --success_hosts Output the hosts that succeeded in summary as wells. (default: False)
155
184
  -g, --greppable Output in greppable format. (default: False)
156
- -nw, --nowatch Do not watch the output in curses modem, Use \r. Not implemented yet. (default: False)
157
- -su, --skipunreachable
158
- Skip unreachable hosts while using --repeat. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto
159
- skip unreachable hosts. (default: False)
160
- -sh SKIPHOSTS, --skiphosts SKIPHOSTS
161
- Skip the hosts in the list. (default: )
185
+ -su, --skip_unreachable
186
+ Skip unreachable hosts while using --repeat. Note: Timedout Hosts are considered unreachable. Note: multiple
187
+ command sequence will still auto skip unreachable hosts. (default: False)
188
+ -sh SKIP_HOSTS, --skip_hosts SKIP_HOSTS
189
+ Skip the hosts in the list. (default: None)
190
+ --store_config_file Store / generate the default config file from command line argument and current config at
191
+ /etc/multiSSH3.config.json
192
+ --debug Print debug information
193
+ --copy-id Copy the ssh id to the hosts
162
194
  -V, --version show program's version number and exit
163
195
  ```
164
196
 
@@ -0,0 +1,7 @@
1
+ multiSSH3.py,sha256=mNEOxE6IVnDu9eHdNfsmY_ZW765SFsf4fuelzG4hBDE,98229
2
+ multiSSH3-5.0.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
3
+ multiSSH3-5.0.dist-info/METADATA,sha256=iYZECVBpY6n8O-y92Z7aN0E8MuuiK4F91KcV8BjenAU,17516
4
+ multiSSH3-5.0.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
5
+ multiSSH3-5.0.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
6
+ multiSSH3-5.0.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
7
+ multiSSH3-5.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (75.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
multiSSH3.py CHANGED
@@ -18,6 +18,7 @@ import glob
18
18
  import shutil
19
19
  import getpass
20
20
  import uuid
21
+ import tempfile
21
22
 
22
23
  try:
23
24
  # Check if functiools.cache is available
@@ -30,7 +31,7 @@ except AttributeError:
30
31
  # If neither is available, use a dummy decorator
31
32
  def cache_decorator(func):
32
33
  return func
33
- version = '4.98'
34
+ version = '5.00'
34
35
  VERSION = version
35
36
 
36
37
  CONFIG_FILE = '/etc/multiSSH3.config.json'
@@ -68,6 +69,9 @@ __build_in_default_config = {
68
69
  'DEFAULT_HOSTS': 'all',
69
70
  'DEFAULT_USERNAME': None,
70
71
  'DEFAULT_PASSWORD': '',
72
+ 'DEFAULT_IDENTITY_FILE': None,
73
+ 'DEDAULT_SSH_KEY_SEARCH_PATH': '~/.ssh/',
74
+ 'DEFAULT_USE_KEY': False,
71
75
  'DEFAULT_EXTRA_ARGS': None,
72
76
  'DEFAULT_ONE_ON_ONE': False,
73
77
  'DEFAULT_SCP': False,
@@ -91,7 +95,7 @@ __build_in_default_config = {
91
95
  'DEFAULT_JSON_MODE': False,
92
96
  'DEFAULT_PRINT_SUCCESS_HOSTS': False,
93
97
  'DEFAULT_GREPPABLE_MODE': False,
94
- 'DEFAULT_SKIP_UNREACHABLE': False,
98
+ 'DEFAULT_SKIP_UNREACHABLE': True,
95
99
  'DEFAULT_SKIP_HOSTS': '',
96
100
  'SSH_STRICT_HOST_KEY_CHECKING': False,
97
101
  'ERROR_MESSAGES_TO_IGNORE': [
@@ -125,6 +129,9 @@ DEFAULT_HOSTS = __configs_from_file.get('DEFAULT_HOSTS', __build_in_default_conf
125
129
  DEFAULT_ENV_FILE = __configs_from_file.get('DEFAULT_ENV_FILE', __build_in_default_config['DEFAULT_ENV_FILE'])
126
130
  DEFAULT_USERNAME = __configs_from_file.get('DEFAULT_USERNAME', __build_in_default_config['DEFAULT_USERNAME'])
127
131
  DEFAULT_PASSWORD = __configs_from_file.get('DEFAULT_PASSWORD', __build_in_default_config['DEFAULT_PASSWORD'])
132
+ DEFAULT_IDENTITY_FILE = __configs_from_file.get('DEFAULT_IDENTITY_FILE', __build_in_default_config['DEFAULT_IDENTITY_FILE'])
133
+ DEDAULT_SSH_KEY_SEARCH_PATH = __configs_from_file.get('DEDAULT_SSH_KEY_SEARCH_PATH', __build_in_default_config['DEDAULT_SSH_KEY_SEARCH_PATH'])
134
+ DEFAULT_USE_KEY = __configs_from_file.get('DEFAULT_USE_KEY', __build_in_default_config['DEFAULT_USE_KEY'])
128
135
  DEFAULT_EXTRA_ARGS = __configs_from_file.get('DEFAULT_EXTRA_ARGS', __build_in_default_config['DEFAULT_EXTRA_ARGS'])
129
136
  DEFAULT_ONE_ON_ONE = __configs_from_file.get('DEFAULT_ONE_ON_ONE', __build_in_default_config['DEFAULT_ONE_ON_ONE'])
130
137
  DEFAULT_SCP = __configs_from_file.get('DEFAULT_SCP', __build_in_default_config['DEFAULT_SCP'])
@@ -173,7 +180,7 @@ __DEBUG_MODE = __configs_from_file.get('__DEBUG_MODE', __build_in_default_config
173
180
 
174
181
 
175
182
 
176
- __global_suppress_printout = True
183
+ __global_suppress_printout = False
177
184
 
178
185
  __mainReturnCode = 0
179
186
  __failedHosts = set()
@@ -193,7 +200,7 @@ def get_i():
193
200
  return __host_i_counter
194
201
 
195
202
  class Host:
196
- def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False):
203
+ def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False,identity_file=None):
197
204
  self.name = name # the name of the host (hostname or IP address)
198
205
  self.command = command # the command to run on the host
199
206
  self.returncode = None # the return code of the command
@@ -212,12 +219,13 @@ class Host:
212
219
  # also store a globally unique integer i from 0
213
220
  self.i = get_i()
214
221
  self.uuid = uuid.uuid4()
222
+ self.identity_file = identity_file
215
223
 
216
224
  def __iter__(self):
217
225
  return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
218
226
  def __repr__(self):
219
227
  # return the complete data structure
220
- return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr}, output={self.output}, printedLines={self.printedLines}, files={self.files}, ipmi={self.ipmi}, interface_ip_prefix={self.interface_ip_prefix}, scp={self.scp}, gatherMode={self.gatherMode}, extraargs={self.extraargs}, resolvedName={self.resolvedName}, i={self.i}, uuid={self.uuid})"
228
+ return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr}, output={self.output}, printedLines={self.printedLines}, files={self.files}, ipmi={self.ipmi}, interface_ip_prefix={self.interface_ip_prefix}, scp={self.scp}, gatherMode={self.gatherMode}, extraargs={self.extraargs}, resolvedName={self.resolvedName}, i={self.i}, uuid={self.uuid}), identity_file={self.identity_file}"
221
229
  def __str__(self):
222
230
  return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr})"
223
231
 
@@ -255,7 +263,7 @@ def check_path(program_name):
255
263
  return True
256
264
  return False
257
265
 
258
- [check_path(program) for program in ['sshpass', 'ssh', 'scp', 'ipmitool','rsync','bash']]
266
+ [check_path(program) for program in ['sshpass', 'ssh', 'scp', 'ipmitool','rsync','bash','ssh-copy-id']]
259
267
 
260
268
 
261
269
 
@@ -678,11 +686,13 @@ def ssh_command(host, sem, timeout=60,passwds=None):
678
686
  global _binPaths
679
687
  global __DEBUG_MODE
680
688
  try:
681
- keyCheckArgs = []
682
- rsyncKeyCheckArgs = []
689
+ localExtraArgs = []
690
+
683
691
  if not SSH_STRICT_HOST_KEY_CHECKING:
684
- keyCheckArgs = ['-o StrictHostKeyChecking=no','-o UserKnownHostsFile=/dev/null']
685
- rsyncKeyCheckArgs = ['--rsh','ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null']
692
+ localExtraArgs = ['-o StrictHostKeyChecking=no','-o UserKnownHostsFile=/dev/null']
693
+ if host.identity_file:
694
+ localExtraArgs += ['-i',host.identity_file]
695
+ rsyncLocalExtraArgs = ['--rsh','ssh ' + ' '.join(localExtraArgs)]
686
696
  host.username = None
687
697
  host.address = host.name
688
698
  if '@' in host.name:
@@ -783,11 +793,11 @@ def ssh_command(host, sem, timeout=60,passwds=None):
783
793
  else:
784
794
  fileArgs = host.files + [f'{host.resolvedName}:{host.command}']
785
795
  if useScp:
786
- formatedCMD = [_binPaths['scp'],'-rpB'] + keyCheckArgs + extraargs +['--']+fileArgs
796
+ formatedCMD = [_binPaths['scp'],'-rpB'] + localExtraArgs + extraargs +['--']+fileArgs
787
797
  else:
788
- formatedCMD = [_binPaths['rsync'],'-ahlX','--partial','--inplace', '--info=name'] + rsyncKeyCheckArgs + extraargs +['--']+fileArgs
798
+ formatedCMD = [_binPaths['rsync'],'-ahlX','--partial','--inplace', '--info=name'] + rsyncLocalExtraArgs + extraargs +['--']+fileArgs
789
799
  else:
790
- formatedCMD = [_binPaths['ssh']] + keyCheckArgs + extraargs +['--']+ [host.resolvedName, host.command]
800
+ formatedCMD = [_binPaths['ssh']] + localExtraArgs + extraargs +['--']+ [host.resolvedName, host.command]
791
801
  if passwds and 'sshpass' in _binPaths:
792
802
  formatedCMD = [_binPaths['sshpass'], '-p', passwds] + formatedCMD
793
803
  elif passwds:
@@ -839,7 +849,7 @@ def ssh_command(host, sem, timeout=60,passwds=None):
839
849
 
840
850
  proc.terminate()
841
851
  break
842
- elif time.time() - host.lastUpdateTime > min(30, timeout // 2):
852
+ elif time.time() - host.lastUpdateTime > max(1, timeout // 2):
843
853
  timeoutLine = f'Timeout in [{timeout - int(time.time() - host.lastUpdateTime)}] seconds!'
844
854
  if host.output and not host.output[-1].strip().startswith(timeoutLine):
845
855
  # remove last line if it is a countdown
@@ -1293,9 +1303,10 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
1293
1303
  outputs[hostPrintOut] = [host['name']]
1294
1304
  else:
1295
1305
  outputs[hostPrintOut].append(host['name'])
1296
- rtnStr = ''
1306
+ rtnStr = '*'*80+'\n'
1297
1307
  for output, hosts in outputs.items():
1298
1308
  rtnStr += f"{','.join(hosts)}{output}\n"
1309
+ rtnStr += '*'*80+'\n'
1299
1310
  if __keyPressesIn[-1]:
1300
1311
  CMDsOut = [''.join(cmd).encode('unicode_escape').decode().replace('\\n', '↵') for cmd in __keyPressesIn if cmd]
1301
1312
  rtnStr += 'User Inputs: '+ '\nUser Inputs: '.join(CMDsOut)
@@ -1411,9 +1422,25 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
1411
1422
  if __DEBUG_MODE:
1412
1423
  print(f'Unreachable hosts: {unavailableHosts}')
1413
1424
  __globalUnavailableHosts.update(unavailableHosts)
1414
- # update the os environment variable if not _no_env
1415
- if not _no_env:
1416
- os.environ['__multiSSH3_UNAVAILABLE_HOSTS'] = ','.join(unavailableHosts)
1425
+
1426
+ # os.environ['__multiSSH3_UNAVAILABLE_HOSTS'] = ','.join(unavailableHosts)
1427
+ # create a temporary file to store the unavailable hosts
1428
+ try:
1429
+ # check for the old content, only update if the new content is different
1430
+ if not os.path.exists(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS')):
1431
+ with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS'),'w') as f:
1432
+ f.write(','.join(unavailableHosts))
1433
+ else:
1434
+ try:
1435
+ with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS'),'r') as f:
1436
+ oldSet = set(f.read().strip().split(','))
1437
+ except:
1438
+ oldSet = None
1439
+ if not oldSet or set(oldSet) != unavailableHosts:
1440
+ with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS'),'w') as f:
1441
+ f.write(','.join(unavailableHosts))
1442
+ except Exception as e:
1443
+ eprint(f'Error writing to temporary file: {e}')
1417
1444
 
1418
1445
  # print the output, if the output of multiple hosts are the same, we aggragate them
1419
1446
  if not called:
@@ -1449,12 +1476,13 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
1449
1476
  files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,
1450
1477
  scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1451
1478
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,skip_hosts = DEFAULT_SKIP_HOSTS,
1452
- file_sync = False, error_only = DEFAULT_ERROR_ONLY,
1479
+ file_sync = False, error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
1453
1480
  shortend = False) -> str:
1454
1481
  argsList = []
1455
1482
  if oneonone: argsList.append('--oneonone' if not shortend else '-11')
1456
1483
  if timeout and timeout != DEFAULT_TIMEOUT: argsList.append(f'--timeout={timeout}' if not shortend else f'-t={timeout}')
1457
1484
  if password and password != DEFAULT_PASSWORD: argsList.append(f'--password="{password}"' if not shortend else f'-p="{password}"')
1485
+ if identity_file and identity_file != DEFAULT_IDENTITY_FILE: argsList.append(f'--key="{identity_file}"' if not shortend else f'-k="{identity_file}"')
1458
1486
  if nowatch: argsList.append('--nowatch' if not shortend else '-q')
1459
1487
  if json: argsList.append('--json' if not shortend else '-j')
1460
1488
  if max_connections and max_connections != DEFAULT_MAX_CONNECTIONS: argsList.append(f'--max_connections={max_connections}' if not shortend else f'-m={max_connections}')
@@ -1479,7 +1507,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
1479
1507
  scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1480
1508
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
1481
1509
  skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
1482
- single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,
1510
+ single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
1483
1511
  shortend = False):
1484
1512
  hosts = hosts if type(hosts) == str else frozenset(hosts)
1485
1513
  hostStr = formHostStr(hosts)
@@ -1488,7 +1516,8 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
1488
1516
  nowatch = nowatch,json = json,max_connections=max_connections,
1489
1517
  files = files,ipmi = ipmi,interface_ip_prefix = interface_ip_prefix,scp=scp,gather_mode = gather_mode,
1490
1518
  username=username,extraargs=extraargs,skipUnreachable=skipUnreachable,no_env=no_env,
1491
- greppable=greppable,skip_hosts = skip_hosts, file_sync = file_sync,error_only = error_only, shortend = shortend)
1519
+ greppable=greppable,skip_hosts = skip_hosts, file_sync = file_sync,error_only = error_only, identity_file = identity_file,
1520
+ shortend = shortend)
1492
1521
  commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
1493
1522
  return f'multissh {argsStr} {hostStr} {commandStr}'
1494
1523
 
@@ -1498,7 +1527,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1498
1527
  scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1499
1528
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
1500
1529
  skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
1501
- single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,quiet = False):
1530
+ single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,quiet = False,identity_file = DEFAULT_IDENTITY_FILE):
1502
1531
  f'''
1503
1532
  Run the command on the hosts, aka multissh. main function
1504
1533
 
@@ -1532,6 +1561,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1532
1561
  file_sync (bool, optional): Whether to use file sync mode to sync directories. Defaults to {DEFAULT_FILE_SYNC}.
1533
1562
  error_only (bool, optional): Whether to only print the error output. Defaults to {DEFAULT_ERROR_ONLY}.
1534
1563
  quiet (bool, optional): Whether to suppress all verbose printout, added for compatibility, avoid using. Defaults to False.
1564
+ identity_file (str, optional): The identity file to use for the ssh connection. Defaults to {DEFAULT_IDENTITY_FILE}.
1535
1565
 
1536
1566
  Returns:
1537
1567
  list: A list of Host objects
@@ -1543,10 +1573,26 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1543
1573
  global __DEBUG_MODE
1544
1574
  _emo = False
1545
1575
  _no_env = no_env
1546
- if not no_env and '__multiSSH3_UNAVAILABLE_HOSTS' in os.environ:
1547
- __globalUnavailableHosts = set(os.environ['__multiSSH3_UNAVAILABLE_HOSTS'].split(','))
1576
+ if os.path.exists(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS')):
1577
+ if timeout <= 0:
1578
+ checkTime = DEFAULT_TIMEOUT
1579
+ else:
1580
+ checkTime = timeout
1581
+ if checkTime <= 0:
1582
+ checkTime = 60
1583
+ try:
1584
+ if 0 < time.time() - os.path.getmtime(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS')) < checkTime:
1585
+ if not __global_suppress_printout:
1586
+ eprint(f"Reading unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS')}")
1587
+ with open(os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS'),'r') as f:
1588
+ __globalUnavailableHosts.update(f.read().strip().split(','))
1589
+ if __DEBUG_MODE:
1590
+ eprint(f"Unavailable hosts: {__globalUnavailableHosts}")
1591
+ except Exception as e:
1592
+ eprint(f"Warning: Unable to read the unavailable hosts from the file {os.path.join(tempfile.gettempdir(),'__multiSSH3_UNAVAILABLE_HOSTS')}")
1593
+ eprint(str(e))
1548
1594
  elif '__multiSSH3_UNAVAILABLE_HOSTS' in readEnvFromFile():
1549
- __globalUnavailableHosts = set(readEnvFromFile()['__multiSSH3_UNAVAILABLE_HOSTS'].split(','))
1595
+ __globalUnavailableHosts.update(readEnvFromFile()['__multiSSH3_UNAVAILABLE_HOSTS'].split(','))
1550
1596
  if not max_connections:
1551
1597
  max_connections = 4 * os.cpu_count()
1552
1598
  elif max_connections == 0:
@@ -1652,9 +1698,9 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1652
1698
  continue
1653
1699
  if host.strip() in skipHostsList: continue
1654
1700
  if file_sync:
1655
- hosts.append(Host(host.strip(), os.path.dirname(command)+os.path.sep, files = [command],ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
1701
+ hosts.append(Host(host.strip(), os.path.dirname(command)+os.path.sep, files = [command],ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode,identity_file=identity_file))
1656
1702
  else:
1657
- hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
1703
+ hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode,identity_file=identity_file))
1658
1704
  if not __global_suppress_printout:
1659
1705
  eprint(f"Running command: {command} on host: {host}")
1660
1706
  if not __global_suppress_printout: print('-'*80)
@@ -1678,7 +1724,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1678
1724
  elif ipmi:
1679
1725
  eprint(f"Error: ipmi mode is not supported in interactive mode")
1680
1726
  else:
1681
- hosts.append(Host(host.strip(), '', files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs))
1727
+ hosts.append(Host(host.strip(), '', files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,identity_file=identity_file))
1682
1728
  if not __global_suppress_printout:
1683
1729
  eprint('-'*80)
1684
1730
  eprint(f"Running in interactive mode on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
@@ -1696,9 +1742,9 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1696
1742
  continue
1697
1743
  if host.strip() in skipHostsList: continue
1698
1744
  if file_sync:
1699
- hosts.append(Host(host.strip(), os.path.dirname(command)+os.path.sep, files = [command],ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
1745
+ hosts.append(Host(host.strip(), os.path.dirname(command)+os.path.sep, files = [command],ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode,identity_file=identity_file))
1700
1746
  else:
1701
- hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
1747
+ hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode,identity_file=identity_file))
1702
1748
  if not __global_suppress_printout and len(commands) > 1:
1703
1749
  eprint('-'*80)
1704
1750
  eprint(f"Running command: {command} on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
@@ -1723,6 +1769,9 @@ def get_default_config(args):
1723
1769
  'DEFAULT_HOSTS': args.hosts,
1724
1770
  'DEFAULT_USERNAME': args.username,
1725
1771
  'DEFAULT_PASSWORD': args.password,
1772
+ 'DEFAULT_IDENTITY_FILE': args.key if args.key and not os.path.isdir(args.key) else DEFAULT_IDENTITY_FILE,
1773
+ 'DEDAULT_SSH_KEY_SEARCH_PATH': args.key if args.key and os.path.isdir(args.key) else DEDAULT_SSH_KEY_SEARCH_PATH,
1774
+ 'DEFAULT_USE_KEY': args.use_key,
1726
1775
  'DEFAULT_EXTRA_ARGS': args.extraargs,
1727
1776
  'DEFAULT_ONE_ON_ONE': args.oneonone,
1728
1777
  'DEFAULT_SCP': args.scp,
@@ -1761,6 +1810,26 @@ def write_default_config(args,CONFIG_FILE,backup = True):
1761
1810
  with open(CONFIG_FILE,'w') as f:
1762
1811
  json.dump(__configs_from_file,f,indent=4)
1763
1812
 
1813
+ def find_ssh_key_file(searchPath = DEDAULT_SSH_KEY_SEARCH_PATH):
1814
+ '''
1815
+ Find the ssh public key file
1816
+
1817
+ Args:
1818
+ searchPath (str, optional): The path to search. Defaults to DEDAULT_SSH_KEY_SEARCH_PATH.
1819
+
1820
+ Returns:
1821
+ str: The path to the ssh key file
1822
+ '''
1823
+ if searchPath:
1824
+ sshKeyPath = searchPath
1825
+ else:
1826
+ sshKeyPath ='~/.ssh'
1827
+ possibleSshKeyFiles = ['id_ed25519','id_ed25519_sk','id_ecdsa','id_ecdsa_sk','id_rsa','id_dsa']
1828
+ for sshKeyFile in possibleSshKeyFiles:
1829
+ if os.path.exists(os.path.expanduser(os.path.join(sshKeyPath,sshKeyFile))):
1830
+ return os.path.join(sshKeyPath,sshKeyFile)
1831
+ return None
1832
+
1764
1833
 
1765
1834
  def main():
1766
1835
  global _emo
@@ -1780,6 +1849,8 @@ def main():
1780
1849
  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.')
1781
1850
  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)
1782
1851
  parser.add_argument('-p', '--password', type=str,help=f'The password to use to connect to the hosts, (default: {DEFAULT_PASSWORD})',default=DEFAULT_PASSWORD)
1852
+ parser.add_argument('-k','--key','--identity',nargs='?', type=str,help=f'The identity file to use to connect to the hosts. Implies --use_key. Specify a folder for program to search for a key. Use option without value to use {DEDAULT_SSH_KEY_SEARCH_PATH} (default: {DEFAULT_IDENTITY_FILE})',const=DEDAULT_SSH_KEY_SEARCH_PATH,default=DEFAULT_IDENTITY_FILE)
1853
+ parser.add_argument('-uk','--use_key', action='store_true', help=f'Attempt to use public key file to connect to the hosts. (default: {DEFAULT_USE_KEY})', default=DEFAULT_USE_KEY)
1783
1854
  parser.add_argument('-ea','--extraargs',type=str,help=f'Extra arguments to pass to the ssh / rsync / scp command. Put in one string for multiple arguments.Use "=" ! Ex. -ea="--delete" (default: {DEFAULT_EXTRA_ARGS})',default=DEFAULT_EXTRA_ARGS)
1784
1855
  parser.add_argument("-11",'--oneonone', action='store_true', help=f"Run one corresponding command on each host. (default: {DEFAULT_ONE_ON_ONE})", default=DEFAULT_ONE_ON_ONE)
1785
1856
  parser.add_argument("-f","--file", action='append', help="The file to be copied to the hosts. Use -f multiple times to copy multiple files")
@@ -1805,7 +1876,7 @@ def main():
1805
1876
  parser.add_argument("-j","--json", action='store_true', help=F"Output in json format. (default: {DEFAULT_JSON_MODE})", default=DEFAULT_JSON_MODE)
1806
1877
  parser.add_argument("--success_hosts", action='store_true', help=f"Output the hosts that succeeded in summary as wells. (default: {DEFAULT_PRINT_SUCCESS_HOSTS})", default=DEFAULT_PRINT_SUCCESS_HOSTS)
1807
1878
  parser.add_argument("-g","--greppable", action='store_true', help=f"Output in greppable format. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
1808
- parser.add_argument("-su","--skip_unreachable", action='store_true', help=f"Skip unreachable hosts while using --repeat. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {DEFAULT_SKIP_UNREACHABLE})", default=DEFAULT_SKIP_UNREACHABLE)
1879
+ parser.add_argument("-su","--skip_unreachable", action='store_true', help=f"Skip unreachable hosts. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {DEFAULT_SKIP_UNREACHABLE})", default=DEFAULT_SKIP_UNREACHABLE)
1809
1880
  parser.add_argument("-sh","--skip_hosts", type=str, help=f"Skip the hosts in the list. (default: {DEFAULT_SKIP_HOSTS if DEFAULT_SKIP_HOSTS else 'None'})", default=DEFAULT_SKIP_HOSTS)
1810
1881
  parser.add_argument('--store_config_file', action='store_true', help=f'Store / generate the default config file from command line argument and current config at {CONFIG_FILE}')
1811
1882
  parser.add_argument('--debug', action='store_true', help='Print debug information')
@@ -1848,6 +1919,8 @@ def main():
1848
1919
  eprint(f"Config file written to {CONFIG_FILE}")
1849
1920
  except Exception as e:
1850
1921
  eprint(f"Error while writing config file: {e}")
1922
+ import traceback
1923
+ eprint(traceback.format_exc())
1851
1924
  if not args.commands:
1852
1925
  with open(CONFIG_FILE,'r') as f:
1853
1926
  eprint(f"Config file content: \n{f.read()}")
@@ -1870,18 +1943,47 @@ def main():
1870
1943
  eprint(f"\nRunning multiple commands: {', '.join(args.commands)} on all hosts")
1871
1944
  else:
1872
1945
  sys.exit(0)
1873
-
1946
+
1947
+ if args.key or args.use_key:
1948
+ if not args.key:
1949
+ args.key = find_ssh_key_file()
1950
+ else:
1951
+ if os.path.isdir(os.path.expanduser(args.key)):
1952
+ args.key = find_ssh_key_file(args.key)
1953
+ elif not os.path.exists(args.key):
1954
+ eprint(f"Warning: Identity file {args.key} not found. Passing to ssh anyway. Proceed with caution.")
1955
+
1956
+ if args.copy_id:
1957
+ if 'ssh-copy-id' in _binPaths:
1958
+ # we will copy the id to the hosts
1959
+ for host in formHostStr(args.hosts).split(','):
1960
+ command = f"{_binPaths['ssh-copy-id']} "
1961
+ if args.key:
1962
+ command = f"{command}-i {args.key} "
1963
+ if args.username:
1964
+ command = f"{command} {args.username}@"
1965
+ command = f"{command}{host}"
1966
+ if args.password and 'sshpass' in _binPaths:
1967
+ command = f"{_binPaths['sshpass']} -p {args.password} {command}"
1968
+ eprint(f"> {command}")
1969
+ os.system(command)
1970
+ else:
1971
+ eprint(f"Warning: ssh-copy-id not found in {_binPaths} , skipping copy id to the hosts")
1972
+ if not args.commands:
1973
+ sys.exit(0)
1974
+
1874
1975
  __ipmiiInterfaceIPPrefix = args.ipmi_interface_ip_prefix
1875
1976
 
1876
- if not args.greppable and not args.json and not args.no_output:
1877
- __global_suppress_printout = False
1977
+ if args.no_output:
1978
+ __global_suppress_printout = True
1878
1979
 
1879
1980
  if not __global_suppress_printout:
1880
- eprint('> ' + getStrCommand(args.hosts,args.commands,oneonone=args.oneonone,timeout=args.timeout,password=args.password,
1981
+ cmdStr = getStrCommand(args.hosts,args.commands,oneonone=args.oneonone,timeout=args.timeout,password=args.password,
1881
1982
  nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
1882
1983
  files=args.file,file_sync=args.file_sync,ipmi=args.ipmi,interface_ip_prefix=args.interface_ip_prefix,scp=args.scp,gather_mode = args.gather_mode,username=args.username,
1883
1984
  extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
1884
- curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only))
1985
+ curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only,identity_file=args.key)
1986
+ eprint('> ' + cmdStr)
1885
1987
  if args.error_only:
1886
1988
  __global_suppress_printout = True
1887
1989
 
@@ -1896,7 +1998,7 @@ def main():
1896
1998
  nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
1897
1999
  files=args.file,file_sync=args.file_sync,ipmi=args.ipmi,interface_ip_prefix=args.interface_ip_prefix,scp=args.scp,gather_mode = args.gather_mode,username=args.username,
1898
2000
  extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
1899
- curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only)
2001
+ curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only,identity_file=args.key)
1900
2002
  #print('*'*80)
1901
2003
 
1902
2004
  if not __global_suppress_printout: eprint('-'*80)
@@ -1,7 +0,0 @@
1
- multiSSH3.py,sha256=Dm7lML6_oR8yedBdlZCsR-J7Et2Xh5r149y1V666Avw,93039
2
- multiSSH3-4.98.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
3
- multiSSH3-4.98.dist-info/METADATA,sha256=hkcvqm3j6w0i24rVinCJMhoxax1btQbHoCaqG-SnJx0,16043
4
- multiSSH3-4.98.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
5
- multiSSH3-4.98.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
6
- multiSSH3-4.98.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
7
- multiSSH3-4.98.dist-info/RECORD,,