multiSSH3 4.97__py3-none-any.whl → 4.99__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.97
3
+ Version: 4.99
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=MdhvFCCWxQCZkjqFcsCp9nwiYiB1jKg1_YLu6BBrqZ4,96689
2
+ multiSSH3-4.99.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
3
+ multiSSH3-4.99.dist-info/METADATA,sha256=cLbJhPOIt81YaMyAn5iOocyPvEST-3RRFP-uafmZm30,17517
4
+ multiSSH3-4.99.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
5
+ multiSSH3-4.99.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
6
+ multiSSH3-4.99.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
7
+ multiSSH3-4.99.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
@@ -30,7 +30,7 @@ except AttributeError:
30
30
  # If neither is available, use a dummy decorator
31
31
  def cache_decorator(func):
32
32
  return func
33
- version = '4.97'
33
+ version = '4.99'
34
34
  VERSION = version
35
35
 
36
36
  CONFIG_FILE = '/etc/multiSSH3.config.json'
@@ -68,6 +68,9 @@ __build_in_default_config = {
68
68
  'DEFAULT_HOSTS': 'all',
69
69
  'DEFAULT_USERNAME': None,
70
70
  'DEFAULT_PASSWORD': '',
71
+ 'DEFAULT_IDENTITY_FILE': None,
72
+ 'DEDAULT_SSH_KEY_SEARCH_PATH': '~/.ssh/',
73
+ 'DEFAULT_USE_KEY': False,
71
74
  'DEFAULT_EXTRA_ARGS': None,
72
75
  'DEFAULT_ONE_ON_ONE': False,
73
76
  'DEFAULT_SCP': False,
@@ -115,6 +118,7 @@ __build_in_default_config = {
115
118
  '_rsyncPath': None,
116
119
  '_bashPath': None,
117
120
  '__ERROR_MESSAGES_TO_IGNORE_REGEX':None,
121
+ '__DEBUG_MODE': False,
118
122
  }
119
123
 
120
124
  AUTHOR = __configs_from_file.get('AUTHOR', __build_in_default_config['AUTHOR'])
@@ -124,6 +128,9 @@ DEFAULT_HOSTS = __configs_from_file.get('DEFAULT_HOSTS', __build_in_default_conf
124
128
  DEFAULT_ENV_FILE = __configs_from_file.get('DEFAULT_ENV_FILE', __build_in_default_config['DEFAULT_ENV_FILE'])
125
129
  DEFAULT_USERNAME = __configs_from_file.get('DEFAULT_USERNAME', __build_in_default_config['DEFAULT_USERNAME'])
126
130
  DEFAULT_PASSWORD = __configs_from_file.get('DEFAULT_PASSWORD', __build_in_default_config['DEFAULT_PASSWORD'])
131
+ DEFAULT_IDENTITY_FILE = __configs_from_file.get('DEFAULT_IDENTITY_FILE', __build_in_default_config['DEFAULT_IDENTITY_FILE'])
132
+ DEDAULT_SSH_KEY_SEARCH_PATH = __configs_from_file.get('DEDAULT_SSH_KEY_SEARCH_PATH', __build_in_default_config['DEDAULT_SSH_KEY_SEARCH_PATH'])
133
+ DEFAULT_USE_KEY = __configs_from_file.get('DEFAULT_USE_KEY', __build_in_default_config['DEFAULT_USE_KEY'])
127
134
  DEFAULT_EXTRA_ARGS = __configs_from_file.get('DEFAULT_EXTRA_ARGS', __build_in_default_config['DEFAULT_EXTRA_ARGS'])
128
135
  DEFAULT_ONE_ON_ONE = __configs_from_file.get('DEFAULT_ONE_ON_ONE', __build_in_default_config['DEFAULT_ONE_ON_ONE'])
129
136
  DEFAULT_SCP = __configs_from_file.get('DEFAULT_SCP', __build_in_default_config['DEFAULT_SCP'])
@@ -168,6 +175,8 @@ if __ERROR_MESSAGES_TO_IGNORE_REGEX:
168
175
  else:
169
176
  __ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile('|'.join(ERROR_MESSAGES_TO_IGNORE))
170
177
 
178
+ __DEBUG_MODE = __configs_from_file.get('__DEBUG_MODE', __build_in_default_config['__DEBUG_MODE'])
179
+
171
180
 
172
181
 
173
182
  __global_suppress_printout = True
@@ -190,9 +199,7 @@ def get_i():
190
199
  return __host_i_counter
191
200
 
192
201
  class Host:
193
- def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False):
194
- global __host_i_counter
195
- global __host_i_lock
202
+ def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False,identity_file=None):
196
203
  self.name = name # the name of the host (hostname or IP address)
197
204
  self.command = command # the command to run on the host
198
205
  self.returncode = None # the return code of the command
@@ -211,12 +218,13 @@ class Host:
211
218
  # also store a globally unique integer i from 0
212
219
  self.i = get_i()
213
220
  self.uuid = uuid.uuid4()
221
+ self.identity_file = identity_file
214
222
 
215
223
  def __iter__(self):
216
224
  return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
217
225
  def __repr__(self):
218
226
  # return the complete data structure
219
- 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})"
227
+ 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}"
220
228
  def __str__(self):
221
229
  return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr})"
222
230
 
@@ -254,7 +262,7 @@ def check_path(program_name):
254
262
  return True
255
263
  return False
256
264
 
257
- [check_path(program) for program in ['sshpass', 'ssh', 'scp', 'ipmitool','rsync','bash']]
265
+ [check_path(program) for program in ['sshpass', 'ssh', 'scp', 'ipmitool','rsync','bash','ssh-copy-id']]
258
266
 
259
267
 
260
268
 
@@ -675,12 +683,15 @@ def ssh_command(host, sem, timeout=60,passwds=None):
675
683
  global __ERROR_MESSAGES_TO_IGNORE_REGEX
676
684
  global __ipmiiInterfaceIPPrefix
677
685
  global _binPaths
686
+ global __DEBUG_MODE
678
687
  try:
679
- keyCheckArgs = []
680
- rsyncKeyCheckArgs = []
688
+ localExtraArgs = []
689
+
681
690
  if not SSH_STRICT_HOST_KEY_CHECKING:
682
- keyCheckArgs = ['-o StrictHostKeyChecking=no','-o UserKnownHostsFile=/dev/null']
683
- rsyncKeyCheckArgs = ['--rsh','ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null']
691
+ localExtraArgs = ['-o StrictHostKeyChecking=no','-o UserKnownHostsFile=/dev/null']
692
+ if host.identity_file:
693
+ localExtraArgs += ['-i',host.identity_file]
694
+ rsyncLocalExtraArgs = ['--rsh','ssh ' + ' '.join(localExtraArgs)]
684
695
  host.username = None
685
696
  host.address = host.name
686
697
  if '@' in host.name:
@@ -737,6 +748,8 @@ def ssh_command(host, sem, timeout=60,passwds=None):
737
748
  formatedCMD = [_binPaths['ipmitool'],f'-H {host.address}',f'-U {host.username}'] + extraargs + [host.command]
738
749
  elif 'ssh' in _binPaths:
739
750
  host.output.append('Ipmitool not found on the local machine! Trying ipmitool on the remote machine...')
751
+ if __DEBUG_MODE:
752
+ host.stderr.append('Ipmitool not found on the local machine! Trying ipmitool on the remote machine...')
740
753
  host.ipmi = False
741
754
  host.interface_ip_prefix = None
742
755
  host.command = 'ipmitool '+host.command if not host.command.startswith('ipmitool ') else host.command
@@ -754,6 +767,8 @@ def ssh_command(host, sem, timeout=60,passwds=None):
754
767
  useScp = True
755
768
  elif 'rsync' in _binPaths:
756
769
  host.output.append('scp not found on the local machine! Trying to use rsync...')
770
+ if __DEBUG_MODE:
771
+ host.stderr.append('scp not found on the local machine! Trying to use rsync...')
757
772
  useScp = False
758
773
  else:
759
774
  host.output.append('scp not found on the local machine! Please install scp or rsync to use file sync mode.')
@@ -764,6 +779,8 @@ def ssh_command(host, sem, timeout=60,passwds=None):
764
779
  useScp = False
765
780
  elif 'scp' in _binPaths:
766
781
  host.output.append('rsync not found on the local machine! Trying to use scp...')
782
+ if __DEBUG_MODE:
783
+ host.stderr.append('rsync not found on the local machine! Trying to use scp...')
767
784
  useScp = True
768
785
  else:
769
786
  host.output.append('rsync not found on the local machine! Please install rsync or scp to use file sync mode.')
@@ -775,16 +792,17 @@ def ssh_command(host, sem, timeout=60,passwds=None):
775
792
  else:
776
793
  fileArgs = host.files + [f'{host.resolvedName}:{host.command}']
777
794
  if useScp:
778
- formatedCMD = [_binPaths['scp'],'-rpB'] + keyCheckArgs + extraargs +['--']+fileArgs
795
+ formatedCMD = [_binPaths['scp'],'-rpB'] + localExtraArgs + extraargs +['--']+fileArgs
779
796
  else:
780
- formatedCMD = [_binPaths['rsync'],'-ahlX','--partial','--inplace', '--info=name'] + rsyncKeyCheckArgs + extraargs +['--']+fileArgs
797
+ formatedCMD = [_binPaths['rsync'],'-ahlX','--partial','--inplace', '--info=name'] + rsyncLocalExtraArgs + extraargs +['--']+fileArgs
781
798
  else:
782
- formatedCMD = [_binPaths['ssh']] + keyCheckArgs + extraargs +['--']+ [host.resolvedName, host.command]
799
+ formatedCMD = [_binPaths['ssh']] + localExtraArgs + extraargs +['--']+ [host.resolvedName, host.command]
783
800
  if passwds and 'sshpass' in _binPaths:
784
801
  formatedCMD = [_binPaths['sshpass'], '-p', passwds] + formatedCMD
785
802
  elif passwds:
786
803
  host.output.append('Warning: sshpass is not available. Please install sshpass to use password authentication.')
787
- #host.stderr.append('Warning: sshpass is not available. Please install sshpass to use password authentication.')
804
+ if __DEBUG_MODE:
805
+ host.stderr.append('Warning: sshpass is not available. Please install sshpass to use password authentication.')
788
806
  host.output.append('Please provide password via live input or use ssh key authentication.')
789
807
  # # try to send the password via __keyPressesIn
790
808
  # __keyPressesIn[-1] = list(passwds) + ['\n']
@@ -802,6 +820,8 @@ def ssh_command(host, sem, timeout=60,passwds=None):
802
820
  with sem:
803
821
  try:
804
822
  host.output.append('Running command: '+' '.join(formatedCMD))
823
+ if __DEBUG_MODE:
824
+ host.stderr.append('Running command: '+' '.join(formatedCMD))
805
825
  #host.stdout = []
806
826
  proc = subprocess.Popen(formatedCMD,stdout=subprocess.PIPE,stderr=subprocess.PIPE,stdin=subprocess.PIPE)
807
827
  # create a thread to handle stdout
@@ -828,7 +848,7 @@ def ssh_command(host, sem, timeout=60,passwds=None):
828
848
 
829
849
  proc.terminate()
830
850
  break
831
- elif time.time() - host.lastUpdateTime > min(30, timeout // 2):
851
+ elif time.time() - host.lastUpdateTime > max(1, timeout // 2):
832
852
  timeoutLine = f'Timeout in [{timeout - int(time.time() - host.lastUpdateTime)}] seconds!'
833
853
  if host.output and not host.output[-1].strip().startswith(timeoutLine):
834
854
  # remove last line if it is a countdown
@@ -890,6 +910,8 @@ def ssh_command(host, sem, timeout=60,passwds=None):
890
910
  if host.ipmi and host.returncode != 0 and any(['Unable to establish IPMI' in line for line in host.stderr]):
891
911
  host.stderr = []
892
912
  host.output.append('IPMI connection failed! Trying SSH connection...')
913
+ if __DEBUG_MODE:
914
+ host.stderr.append('IPMI connection failed! Trying SSH connection...')
893
915
  host.ipmi = False
894
916
  host.interface_ip_prefix = None
895
917
  host.command = 'ipmitool '+host.command if not host.command.startswith('ipmitool ') else host.command
@@ -899,6 +921,8 @@ def ssh_command(host, sem, timeout=60,passwds=None):
899
921
  host.stderr = []
900
922
  host.stdout = []
901
923
  host.output.append('Rsync connection failed! Trying SCP connection...')
924
+ if __DEBUG_MODE:
925
+ host.stderr.append('Rsync connection failed! Trying SCP connection...')
902
926
  host.scp = True
903
927
  ssh_command(host,sem,timeout,passwds)
904
928
 
@@ -1393,6 +1417,8 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
1393
1417
  # update the unavailable hosts and global unavailable hosts
1394
1418
  if willUpdateUnreachableHosts:
1395
1419
  unavailableHosts.update([host.name for host in hosts if host.stderr and ('No route to host' in host.stderr[0].strip() or host.stderr[0].strip().startswith('Timeout!'))])
1420
+ if __DEBUG_MODE:
1421
+ print(f'Unreachable hosts: {unavailableHosts}')
1396
1422
  __globalUnavailableHosts.update(unavailableHosts)
1397
1423
  # update the os environment variable if not _no_env
1398
1424
  if not _no_env:
@@ -1432,12 +1458,13 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
1432
1458
  files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,
1433
1459
  scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1434
1460
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,skip_hosts = DEFAULT_SKIP_HOSTS,
1435
- file_sync = False, error_only = DEFAULT_ERROR_ONLY,
1461
+ file_sync = False, error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
1436
1462
  shortend = False) -> str:
1437
1463
  argsList = []
1438
1464
  if oneonone: argsList.append('--oneonone' if not shortend else '-11')
1439
1465
  if timeout and timeout != DEFAULT_TIMEOUT: argsList.append(f'--timeout={timeout}' if not shortend else f'-t={timeout}')
1440
1466
  if password and password != DEFAULT_PASSWORD: argsList.append(f'--password="{password}"' if not shortend else f'-p="{password}"')
1467
+ if identity_file and identity_file != DEFAULT_IDENTITY_FILE: argsList.append(f'--key="{identity_file}"' if not shortend else f'-k="{identity_file}"')
1441
1468
  if nowatch: argsList.append('--nowatch' if not shortend else '-q')
1442
1469
  if json: argsList.append('--json' if not shortend else '-j')
1443
1470
  if max_connections and max_connections != DEFAULT_MAX_CONNECTIONS: argsList.append(f'--max_connections={max_connections}' if not shortend else f'-m={max_connections}')
@@ -1462,7 +1489,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
1462
1489
  scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1463
1490
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
1464
1491
  skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
1465
- single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,
1492
+ single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
1466
1493
  shortend = False):
1467
1494
  hosts = hosts if type(hosts) == str else frozenset(hosts)
1468
1495
  hostStr = formHostStr(hosts)
@@ -1471,7 +1498,8 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
1471
1498
  nowatch = nowatch,json = json,max_connections=max_connections,
1472
1499
  files = files,ipmi = ipmi,interface_ip_prefix = interface_ip_prefix,scp=scp,gather_mode = gather_mode,
1473
1500
  username=username,extraargs=extraargs,skipUnreachable=skipUnreachable,no_env=no_env,
1474
- greppable=greppable,skip_hosts = skip_hosts, file_sync = file_sync,error_only = error_only, shortend = shortend)
1501
+ greppable=greppable,skip_hosts = skip_hosts, file_sync = file_sync,error_only = error_only, identity_file = identity_file,
1502
+ shortend = shortend)
1475
1503
  commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
1476
1504
  return f'multissh {argsStr} {hostStr} {commandStr}'
1477
1505
 
@@ -1481,7 +1509,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1481
1509
  scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1482
1510
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
1483
1511
  skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
1484
- single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,quiet = False):
1512
+ single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,quiet = False,identity_file = DEFAULT_IDENTITY_FILE):
1485
1513
  f'''
1486
1514
  Run the command on the hosts, aka multissh. main function
1487
1515
 
@@ -1515,6 +1543,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1515
1543
  file_sync (bool, optional): Whether to use file sync mode to sync directories. Defaults to {DEFAULT_FILE_SYNC}.
1516
1544
  error_only (bool, optional): Whether to only print the error output. Defaults to {DEFAULT_ERROR_ONLY}.
1517
1545
  quiet (bool, optional): Whether to suppress all verbose printout, added for compatibility, avoid using. Defaults to False.
1546
+ identity_file (str, optional): The identity file to use for the ssh connection. Defaults to {DEFAULT_IDENTITY_FILE}.
1518
1547
 
1519
1548
  Returns:
1520
1549
  list: A list of Host objects
@@ -1523,6 +1552,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1523
1552
  global __global_suppress_printout
1524
1553
  global _no_env
1525
1554
  global _emo
1555
+ global __DEBUG_MODE
1526
1556
  _emo = False
1527
1557
  _no_env = no_env
1528
1558
  if not no_env and '__multiSSH3_UNAVAILABLE_HOSTS' in os.environ:
@@ -1585,6 +1615,8 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1585
1615
  skipHostStr[i] = userStr + host
1586
1616
  skipHostStr = ','.join(skipHostStr)
1587
1617
  targetHostsList = expand_hostnames(frozenset(hostStr.split(',')))
1618
+ if __DEBUG_MODE:
1619
+ eprint(f"Target hosts: {targetHostsList}")
1588
1620
  skipHostsList = expand_hostnames(frozenset(skipHostStr.split(',')))
1589
1621
  if skipHostsList:
1590
1622
  eprint(f"Skipping hosts: {skipHostsList}")
@@ -1614,6 +1646,8 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1614
1646
  files = []
1615
1647
  else:
1616
1648
  files = list(pathSet)
1649
+ if __DEBUG_MODE:
1650
+ eprint(f"Files: {files}")
1617
1651
  if oneonone:
1618
1652
  hosts = []
1619
1653
  if len(commands) != len(targetHostsList) - len(skipHostsList):
@@ -1630,9 +1664,9 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1630
1664
  continue
1631
1665
  if host.strip() in skipHostsList: continue
1632
1666
  if file_sync:
1633
- 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))
1667
+ 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))
1634
1668
  else:
1635
- hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
1669
+ 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))
1636
1670
  if not __global_suppress_printout:
1637
1671
  eprint(f"Running command: {command} on host: {host}")
1638
1672
  if not __global_suppress_printout: print('-'*80)
@@ -1656,7 +1690,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1656
1690
  elif ipmi:
1657
1691
  eprint(f"Error: ipmi mode is not supported in interactive mode")
1658
1692
  else:
1659
- hosts.append(Host(host.strip(), '', files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs))
1693
+ hosts.append(Host(host.strip(), '', files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,identity_file=identity_file))
1660
1694
  if not __global_suppress_printout:
1661
1695
  eprint('-'*80)
1662
1696
  eprint(f"Running in interactive mode on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
@@ -1674,9 +1708,9 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1674
1708
  continue
1675
1709
  if host.strip() in skipHostsList: continue
1676
1710
  if file_sync:
1677
- 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))
1711
+ 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))
1678
1712
  else:
1679
- hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs,gatherMode=gather_mode))
1713
+ 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))
1680
1714
  if not __global_suppress_printout and len(commands) > 1:
1681
1715
  eprint('-'*80)
1682
1716
  eprint(f"Running command: {command} on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
@@ -1701,6 +1735,9 @@ def get_default_config(args):
1701
1735
  'DEFAULT_HOSTS': args.hosts,
1702
1736
  'DEFAULT_USERNAME': args.username,
1703
1737
  'DEFAULT_PASSWORD': args.password,
1738
+ 'DEFAULT_IDENTITY_FILE': args.key if args.key and not os.path.isdir(args.key) else DEFAULT_IDENTITY_FILE,
1739
+ 'DEDAULT_SSH_KEY_SEARCH_PATH': args.key if args.key and os.path.isdir(args.key) else DEDAULT_SSH_KEY_SEARCH_PATH,
1740
+ 'DEFAULT_USE_KEY': args.use_key,
1704
1741
  'DEFAULT_EXTRA_ARGS': args.extraargs,
1705
1742
  'DEFAULT_ONE_ON_ONE': args.oneonone,
1706
1743
  'DEFAULT_SCP': args.scp,
@@ -1739,6 +1776,26 @@ def write_default_config(args,CONFIG_FILE,backup = True):
1739
1776
  with open(CONFIG_FILE,'w') as f:
1740
1777
  json.dump(__configs_from_file,f,indent=4)
1741
1778
 
1779
+ def find_ssh_key_file(searchPath = DEDAULT_SSH_KEY_SEARCH_PATH):
1780
+ '''
1781
+ Find the ssh public key file
1782
+
1783
+ Args:
1784
+ searchPath (str, optional): The path to search. Defaults to DEDAULT_SSH_KEY_SEARCH_PATH.
1785
+
1786
+ Returns:
1787
+ str: The path to the ssh key file
1788
+ '''
1789
+ if searchPath:
1790
+ sshKeyPath = searchPath
1791
+ else:
1792
+ sshKeyPath ='~/.ssh'
1793
+ possibleSshKeyFiles = ['id_ed25519','id_ed25519_sk','id_ecdsa','id_ecdsa_sk','id_rsa','id_dsa']
1794
+ for sshKeyFile in possibleSshKeyFiles:
1795
+ if os.path.exists(os.path.expanduser(os.path.join(sshKeyPath,sshKeyFile))):
1796
+ return os.path.join(sshKeyPath,sshKeyFile)
1797
+ return None
1798
+
1742
1799
 
1743
1800
  def main():
1744
1801
  global _emo
@@ -1748,15 +1805,18 @@ def main():
1748
1805
  global __ipmiiInterfaceIPPrefix
1749
1806
  global _binPaths
1750
1807
  global _env_file
1808
+ global __DEBUG_MODE
1751
1809
  _emo = False
1752
1810
  # We handle the signal
1753
1811
  signal.signal(signal.SIGINT, signal_handler)
1754
1812
  # We parse the arguments
1755
1813
  parser = argparse.ArgumentParser(description=f'Run a command on multiple hosts, Use #HOST# or #HOSTNAME# to replace the host name in the command. Config file: {CONFIG_FILE}')
1756
1814
  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)
1757
- 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.')
1815
+ 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.')
1758
1816
  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)
1759
1817
  parser.add_argument('-p', '--password', type=str,help=f'The password to use to connect to the hosts, (default: {DEFAULT_PASSWORD})',default=DEFAULT_PASSWORD)
1818
+ 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)
1819
+ 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)
1760
1820
  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)
1761
1821
  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)
1762
1822
  parser.add_argument("-f","--file", action='append', help="The file to be copied to the hosts. Use -f multiple times to copy multiple files")
@@ -1785,11 +1845,26 @@ def main():
1785
1845
  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)
1786
1846
  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)
1787
1847
  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}')
1848
+ parser.add_argument('--debug', action='store_true', help='Print debug information')
1849
+ parser.add_argument('--copy-id', action='store_true', help='Copy the ssh id to the hosts')
1788
1850
  parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} with [ {", ".join(_binPaths.keys())} ] by {AUTHOR} ({AUTHOR_EMAIL})')
1789
1851
 
1790
1852
  # parser.add_argument('-u', '--user', metavar='user', type=str, nargs=1,
1791
1853
  # help='the user to use to connect to the hosts')
1792
- args = parser.parse_args()
1854
+ #args = parser.parse_args()
1855
+
1856
+ # if python version is 3.7 or higher, use parse_intermixed_args
1857
+ if sys.version_info >= (3,7):
1858
+ args = parser.parse_intermixed_args()
1859
+ else:
1860
+ # try to parse the arguments using parse_known_args
1861
+ args, unknown = parser.parse_known_args()
1862
+ # if there are unknown arguments, we will try to parse them again using parse_args
1863
+ if unknown:
1864
+ eprint(f"Warning: Unknown arguments, treating all as commands: {unknown}")
1865
+ args.commands += unknown
1866
+
1867
+
1793
1868
 
1794
1869
  if args.store_config_file:
1795
1870
  try:
@@ -1810,12 +1885,15 @@ def main():
1810
1885
  eprint(f"Config file written to {CONFIG_FILE}")
1811
1886
  except Exception as e:
1812
1887
  eprint(f"Error while writing config file: {e}")
1888
+ import traceback
1889
+ eprint(traceback.format_exc())
1813
1890
  if not args.commands:
1814
1891
  with open(CONFIG_FILE,'r') as f:
1815
1892
  eprint(f"Config file content: \n{f.read()}")
1816
1893
  sys.exit(0)
1817
1894
 
1818
1895
  _env_file = args.env_file
1896
+ __DEBUG_MODE = args.debug
1819
1897
  # if there are more than 1 commands, and every command only consists of one word,
1820
1898
  # we will ask the user to confirm if they want to run multiple commands or just one command.
1821
1899
  if not args.file and len(args.commands) > 1 and all([len(command.split()) == 1 for command in args.commands]):
@@ -1831,7 +1909,35 @@ def main():
1831
1909
  eprint(f"\nRunning multiple commands: {', '.join(args.commands)} on all hosts")
1832
1910
  else:
1833
1911
  sys.exit(0)
1834
-
1912
+
1913
+ if args.key or args.use_key:
1914
+ if not args.key:
1915
+ args.key = find_ssh_key_file()
1916
+ else:
1917
+ if os.path.isdir(os.path.expanduser(args.key)):
1918
+ args.key = find_ssh_key_file(args.key)
1919
+ elif not os.path.exists(args.key):
1920
+ eprint(f"Warning: Identity file {args.key} not found. Passing to ssh anyway. Proceed with caution.")
1921
+
1922
+ if args.copy_id:
1923
+ if 'ssh-copy-id' in _binPaths:
1924
+ # we will copy the id to the hosts
1925
+ for host in formHostStr(args.hosts).split(','):
1926
+ command = f"{_binPaths['ssh-copy-id']} "
1927
+ if args.key:
1928
+ command = f"{command}-i {args.key} "
1929
+ if args.username:
1930
+ command = f"{command} {args.username}@"
1931
+ command = f"{command}{host}"
1932
+ if args.password and 'sshpass' in _binPaths:
1933
+ command = f"{_binPaths['sshpass']} -p {args.password} {command}"
1934
+ eprint(f"> {command}")
1935
+ os.system(command)
1936
+ else:
1937
+ eprint(f"Warning: ssh-copy-id not found in {_binPaths} , skipping copy id to the hosts")
1938
+ if not args.commands:
1939
+ sys.exit(0)
1940
+
1835
1941
  __ipmiiInterfaceIPPrefix = args.ipmi_interface_ip_prefix
1836
1942
 
1837
1943
  if not args.greppable and not args.json and not args.no_output:
@@ -1842,7 +1948,7 @@ def main():
1842
1948
  nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
1843
1949
  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,
1844
1950
  extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
1845
- curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only))
1951
+ 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))
1846
1952
  if args.error_only:
1847
1953
  __global_suppress_printout = True
1848
1954
 
@@ -1857,7 +1963,7 @@ def main():
1857
1963
  nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
1858
1964
  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,
1859
1965
  extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
1860
- curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only)
1966
+ 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)
1861
1967
  #print('*'*80)
1862
1968
 
1863
1969
  if not __global_suppress_printout: eprint('-'*80)
@@ -1,7 +0,0 @@
1
- multiSSH3.py,sha256=WCp2vui9NMeviCPe39CVu9WY0-lOn3YEIzFGO8i8m7o,91417
2
- multiSSH3-4.97.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
3
- multiSSH3-4.97.dist-info/METADATA,sha256=ndgCxl2LHcQ3x88vqxJhTOd6ah7U46-Xva-Uj74oBzU,16043
4
- multiSSH3-4.97.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
5
- multiSSH3-4.97.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
6
- multiSSH3-4.97.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
7
- multiSSH3-4.97.dist-info/RECORD,,