multiSSH3 4.98__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.98
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.98'
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,
@@ -125,6 +128,9 @@ DEFAULT_HOSTS = __configs_from_file.get('DEFAULT_HOSTS', __build_in_default_conf
125
128
  DEFAULT_ENV_FILE = __configs_from_file.get('DEFAULT_ENV_FILE', __build_in_default_config['DEFAULT_ENV_FILE'])
126
129
  DEFAULT_USERNAME = __configs_from_file.get('DEFAULT_USERNAME', __build_in_default_config['DEFAULT_USERNAME'])
127
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'])
128
134
  DEFAULT_EXTRA_ARGS = __configs_from_file.get('DEFAULT_EXTRA_ARGS', __build_in_default_config['DEFAULT_EXTRA_ARGS'])
129
135
  DEFAULT_ONE_ON_ONE = __configs_from_file.get('DEFAULT_ONE_ON_ONE', __build_in_default_config['DEFAULT_ONE_ON_ONE'])
130
136
  DEFAULT_SCP = __configs_from_file.get('DEFAULT_SCP', __build_in_default_config['DEFAULT_SCP'])
@@ -193,7 +199,7 @@ def get_i():
193
199
  return __host_i_counter
194
200
 
195
201
  class Host:
196
- def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False):
202
+ def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False,identity_file=None):
197
203
  self.name = name # the name of the host (hostname or IP address)
198
204
  self.command = command # the command to run on the host
199
205
  self.returncode = None # the return code of the command
@@ -212,12 +218,13 @@ class Host:
212
218
  # also store a globally unique integer i from 0
213
219
  self.i = get_i()
214
220
  self.uuid = uuid.uuid4()
221
+ self.identity_file = identity_file
215
222
 
216
223
  def __iter__(self):
217
224
  return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
218
225
  def __repr__(self):
219
226
  # 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})"
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}"
221
228
  def __str__(self):
222
229
  return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr})"
223
230
 
@@ -255,7 +262,7 @@ def check_path(program_name):
255
262
  return True
256
263
  return False
257
264
 
258
- [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']]
259
266
 
260
267
 
261
268
 
@@ -678,11 +685,13 @@ def ssh_command(host, sem, timeout=60,passwds=None):
678
685
  global _binPaths
679
686
  global __DEBUG_MODE
680
687
  try:
681
- keyCheckArgs = []
682
- rsyncKeyCheckArgs = []
688
+ localExtraArgs = []
689
+
683
690
  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']
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)]
686
695
  host.username = None
687
696
  host.address = host.name
688
697
  if '@' in host.name:
@@ -783,11 +792,11 @@ def ssh_command(host, sem, timeout=60,passwds=None):
783
792
  else:
784
793
  fileArgs = host.files + [f'{host.resolvedName}:{host.command}']
785
794
  if useScp:
786
- formatedCMD = [_binPaths['scp'],'-rpB'] + keyCheckArgs + extraargs +['--']+fileArgs
795
+ formatedCMD = [_binPaths['scp'],'-rpB'] + localExtraArgs + extraargs +['--']+fileArgs
787
796
  else:
788
- formatedCMD = [_binPaths['rsync'],'-ahlX','--partial','--inplace', '--info=name'] + rsyncKeyCheckArgs + extraargs +['--']+fileArgs
797
+ formatedCMD = [_binPaths['rsync'],'-ahlX','--partial','--inplace', '--info=name'] + rsyncLocalExtraArgs + extraargs +['--']+fileArgs
789
798
  else:
790
- formatedCMD = [_binPaths['ssh']] + keyCheckArgs + extraargs +['--']+ [host.resolvedName, host.command]
799
+ formatedCMD = [_binPaths['ssh']] + localExtraArgs + extraargs +['--']+ [host.resolvedName, host.command]
791
800
  if passwds and 'sshpass' in _binPaths:
792
801
  formatedCMD = [_binPaths['sshpass'], '-p', passwds] + formatedCMD
793
802
  elif passwds:
@@ -839,7 +848,7 @@ def ssh_command(host, sem, timeout=60,passwds=None):
839
848
 
840
849
  proc.terminate()
841
850
  break
842
- elif time.time() - host.lastUpdateTime > min(30, timeout // 2):
851
+ elif time.time() - host.lastUpdateTime > max(1, timeout // 2):
843
852
  timeoutLine = f'Timeout in [{timeout - int(time.time() - host.lastUpdateTime)}] seconds!'
844
853
  if host.output and not host.output[-1].strip().startswith(timeoutLine):
845
854
  # remove last line if it is a countdown
@@ -1449,12 +1458,13 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
1449
1458
  files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,
1450
1459
  scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1451
1460
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,skip_hosts = DEFAULT_SKIP_HOSTS,
1452
- file_sync = False, error_only = DEFAULT_ERROR_ONLY,
1461
+ file_sync = False, error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
1453
1462
  shortend = False) -> str:
1454
1463
  argsList = []
1455
1464
  if oneonone: argsList.append('--oneonone' if not shortend else '-11')
1456
1465
  if timeout and timeout != DEFAULT_TIMEOUT: argsList.append(f'--timeout={timeout}' if not shortend else f'-t={timeout}')
1457
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}"')
1458
1468
  if nowatch: argsList.append('--nowatch' if not shortend else '-q')
1459
1469
  if json: argsList.append('--json' if not shortend else '-j')
1460
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}')
@@ -1479,7 +1489,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
1479
1489
  scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1480
1490
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
1481
1491
  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,
1492
+ single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
1483
1493
  shortend = False):
1484
1494
  hosts = hosts if type(hosts) == str else frozenset(hosts)
1485
1495
  hostStr = formHostStr(hosts)
@@ -1488,7 +1498,8 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
1488
1498
  nowatch = nowatch,json = json,max_connections=max_connections,
1489
1499
  files = files,ipmi = ipmi,interface_ip_prefix = interface_ip_prefix,scp=scp,gather_mode = gather_mode,
1490
1500
  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)
1501
+ greppable=greppable,skip_hosts = skip_hosts, file_sync = file_sync,error_only = error_only, identity_file = identity_file,
1502
+ shortend = shortend)
1492
1503
  commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
1493
1504
  return f'multissh {argsStr} {hostStr} {commandStr}'
1494
1505
 
@@ -1498,7 +1509,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1498
1509
  scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1499
1510
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
1500
1511
  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):
1512
+ single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,quiet = False,identity_file = DEFAULT_IDENTITY_FILE):
1502
1513
  f'''
1503
1514
  Run the command on the hosts, aka multissh. main function
1504
1515
 
@@ -1532,6 +1543,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1532
1543
  file_sync (bool, optional): Whether to use file sync mode to sync directories. Defaults to {DEFAULT_FILE_SYNC}.
1533
1544
  error_only (bool, optional): Whether to only print the error output. Defaults to {DEFAULT_ERROR_ONLY}.
1534
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}.
1535
1547
 
1536
1548
  Returns:
1537
1549
  list: A list of Host objects
@@ -1652,9 +1664,9 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1652
1664
  continue
1653
1665
  if host.strip() in skipHostsList: continue
1654
1666
  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))
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))
1656
1668
  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))
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))
1658
1670
  if not __global_suppress_printout:
1659
1671
  eprint(f"Running command: {command} on host: {host}")
1660
1672
  if not __global_suppress_printout: print('-'*80)
@@ -1678,7 +1690,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1678
1690
  elif ipmi:
1679
1691
  eprint(f"Error: ipmi mode is not supported in interactive mode")
1680
1692
  else:
1681
- 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))
1682
1694
  if not __global_suppress_printout:
1683
1695
  eprint('-'*80)
1684
1696
  eprint(f"Running in interactive mode on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
@@ -1696,9 +1708,9 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1696
1708
  continue
1697
1709
  if host.strip() in skipHostsList: continue
1698
1710
  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))
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))
1700
1712
  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))
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))
1702
1714
  if not __global_suppress_printout and len(commands) > 1:
1703
1715
  eprint('-'*80)
1704
1716
  eprint(f"Running command: {command} on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
@@ -1723,6 +1735,9 @@ def get_default_config(args):
1723
1735
  'DEFAULT_HOSTS': args.hosts,
1724
1736
  'DEFAULT_USERNAME': args.username,
1725
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,
1726
1741
  'DEFAULT_EXTRA_ARGS': args.extraargs,
1727
1742
  'DEFAULT_ONE_ON_ONE': args.oneonone,
1728
1743
  'DEFAULT_SCP': args.scp,
@@ -1761,6 +1776,26 @@ def write_default_config(args,CONFIG_FILE,backup = True):
1761
1776
  with open(CONFIG_FILE,'w') as f:
1762
1777
  json.dump(__configs_from_file,f,indent=4)
1763
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
+
1764
1799
 
1765
1800
  def main():
1766
1801
  global _emo
@@ -1780,6 +1815,8 @@ def main():
1780
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.')
1781
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)
1782
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)
1783
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)
1784
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)
1785
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")
@@ -1848,6 +1885,8 @@ def main():
1848
1885
  eprint(f"Config file written to {CONFIG_FILE}")
1849
1886
  except Exception as e:
1850
1887
  eprint(f"Error while writing config file: {e}")
1888
+ import traceback
1889
+ eprint(traceback.format_exc())
1851
1890
  if not args.commands:
1852
1891
  with open(CONFIG_FILE,'r') as f:
1853
1892
  eprint(f"Config file content: \n{f.read()}")
@@ -1870,7 +1909,35 @@ def main():
1870
1909
  eprint(f"\nRunning multiple commands: {', '.join(args.commands)} on all hosts")
1871
1910
  else:
1872
1911
  sys.exit(0)
1873
-
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
+
1874
1941
  __ipmiiInterfaceIPPrefix = args.ipmi_interface_ip_prefix
1875
1942
 
1876
1943
  if not args.greppable and not args.json and not args.no_output:
@@ -1881,7 +1948,7 @@ def main():
1881
1948
  nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
1882
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,
1883
1950
  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))
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))
1885
1952
  if args.error_only:
1886
1953
  __global_suppress_printout = True
1887
1954
 
@@ -1896,7 +1963,7 @@ def main():
1896
1963
  nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
1897
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,
1898
1965
  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)
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)
1900
1967
  #print('*'*80)
1901
1968
 
1902
1969
  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,,