multiSSH3 4.83__py3-none-any.whl → 4.89__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.83
3
+ Version: 4.89
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
@@ -20,12 +20,12 @@ Can be used in bash scripts for automation actions.
20
20
  Also able to be imported and / or use with Flexec SSH Backend to perform cluster automation actions.
21
21
 
22
22
  Install via
23
- ```
23
+ ```bash
24
24
  pip install multiSSH3
25
25
  ```
26
26
 
27
27
  multiSSH3 will be available as
28
- ```
28
+ ```bash
29
29
  mssh
30
30
  mssh3
31
31
  multissh
@@ -37,7 +37,7 @@ multissh will read a config file located at ```/etc/multiSSH3.config.json```
37
37
 
38
38
  To store / generate a config file with the current command line options, you can use
39
39
 
40
- ```
40
+ ```bash
41
41
  mssh --generate_default_config_file
42
42
  ```
43
43
 
@@ -49,42 +49,52 @@ If you want to store password, it will be a plain text password in this config f
49
49
 
50
50
  This option can also be used to store cli options into the config files. For example.
51
51
 
52
- ```
52
+ ```bash
53
53
  mssh --ipmi_interface_ip_prefix 192 --generate_default_config_file
54
54
  ```
55
55
  will store
56
- ```
56
+ ```json
57
57
  "DEFAULT_IPMI_INTERFACE_IP_PREFIX": "192"
58
58
  ```
59
59
  into the json file.
60
60
 
61
61
  By defualt reads bash env variables for hostname aliases. Also able to read
62
- ```
62
+ ```bash
63
63
  DEFAULT_ENV_FILE = '/etc/profile.d/hosts.sh'
64
64
  ```
65
65
  as hostname aliases.
66
66
 
67
67
  For example:
68
- ```
68
+ ```bash
69
69
  export all='192.168.1-2.1-64'
70
70
  mssh all 'echo hi'
71
71
  ```
72
72
 
73
+ Note: you probably want to set presistent ssh connections to speed up each connection events.
74
+ An example .ssh/config:
75
+ ```bash
76
+ Host *
77
+ StrictHostKeyChecking no
78
+ ControlMaster auto
79
+ ControlPath /run/ssh_sockets_%r@%h-%p
80
+ ControlPersist 3600
81
+ ```
82
+
73
83
  It is also able to recognize ip blocks / number blocks / hex blocks / character blocks directly.
74
84
 
75
85
  For example:
76
- ```
86
+ ```bash
77
87
  mssh testrig[1-10] lsblk
78
88
  mssh ww[a-c],10.100.0.* 'cat /etc/fstab' 'sed -i "/lustre/d' /etc/fstab' 'cat /etc/fstab'
79
89
  ```
80
90
 
81
91
  It also supports interactive inputs. ( and able to async boardcast to all supplied hosts )
82
- ```
92
+ ```bash
83
93
  mssh www bash
84
94
  ```
85
95
 
86
96
  By default, it will try to fit everything inside your window.
87
- ```
97
+ ```bash
88
98
  DEFAULT_CURSES_MINIMUM_CHAR_LEN = 40
89
99
  DEFAULT_CURSES_MINIMUM_LINE_LEN = 1
90
100
  ```
@@ -93,7 +103,7 @@ While leaving minimum 40 characters / 1 line for each host display by default. Y
93
103
 
94
104
  Use ```mssh --help``` for more info.
95
105
 
96
- ```
106
+ ```bash
97
107
  usage: mssh [-h] [-u USERNAME] [-ea EXTRAARGS] [-p PASSWORD] [-11] [-f FILE] [--file_sync] [--scp] [-t TIMEOUT] [-r REPEAT] [-i INTERVAL] [--ipmi]
98
108
  [-pre INTERFACE_IP_PREFIX] [-q] [-ww WINDOW_WIDTH] [-wh WINDOW_HEIGHT] [-sw] [-eo] [-no] [--no_env] [--env_file ENV_FILE] [-m MAXCONNECTIONS] [-j]
99
109
  [--success_hosts] [-g] [-nw] [-su] [-sh SKIPHOSTS] [-V]
@@ -307,8 +317,6 @@ Suppresses all output, useful for scripts where you only care about exit codes.
307
317
  - Use `--no_env` to prevent loading any environment variables from files.
308
318
 
309
319
  ## Notes
310
-
311
- - **SSH Configuration**: The script modifies `~/.ssh/config` to disable `StrictHostKeyChecking`. Ensure this is acceptable in your environment.
312
320
  - **Dependencies**: Requires Python 3, `sshpass` (if using password authentication), and standard Unix utilities like `ssh`, `scp`, and `rsync`.
313
321
  - **Signal Handling**: Supports graceful termination with `Ctrl+C`.
314
322
 
@@ -0,0 +1,7 @@
1
+ multiSSH3.py,sha256=g_U5Kk6pDABART5Tb4yoViPZ-5fiARpb_8Irvs7f2QA,86990
2
+ multiSSH3-4.89.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
3
+ multiSSH3-4.89.dist-info/METADATA,sha256=J9K4EyDzcP7Pvj4p779YAkYz7vbDGfQoxz9hmkWRra8,16043
4
+ multiSSH3-4.89.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
5
+ multiSSH3-4.89.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
6
+ multiSSH3-4.89.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
7
+ multiSSH3-4.89.dist-info/RECORD,,
multiSSH3.py CHANGED
@@ -29,7 +29,7 @@ except AttributeError:
29
29
  # If neither is available, use a dummy decorator
30
30
  def cache_decorator(func):
31
31
  return func
32
- version = '4.83'
32
+ version = '4.89'
33
33
  VERSION = version
34
34
 
35
35
  CONFIG_FILE = '/etc/multiSSH3.config.json'
@@ -83,6 +83,7 @@ __build_in_default_config = {
83
83
  'DEFAULT_GREPPABLE_MODE': False,
84
84
  'DEFAULT_SKIP_UNREACHABLE': False,
85
85
  'DEFAULT_SKIP_HOSTS': '',
86
+ 'SSH_STRICT_HOST_KEY_CHECKING': False,
86
87
  'ERROR_MESSAGES_TO_IGNORE': [
87
88
  'Pseudo-terminal will not be allocated because stdin is not a terminal',
88
89
  'Connection to .* closed',
@@ -140,6 +141,8 @@ DEFAULT_GREPPABLE_MODE = __configs_from_file.get('DEFAULT_GREPPABLE_MODE', __bui
140
141
  DEFAULT_SKIP_UNREACHABLE = __configs_from_file.get('DEFAULT_SKIP_UNREACHABLE', __build_in_default_config['DEFAULT_SKIP_UNREACHABLE'])
141
142
  DEFAULT_SKIP_HOSTS = __configs_from_file.get('DEFAULT_SKIP_HOSTS', __build_in_default_config['DEFAULT_SKIP_HOSTS'])
142
143
 
144
+ SSH_STRICT_HOST_KEY_CHECKING = __configs_from_file.get('SSH_STRICT_HOST_KEY_CHECKING', __build_in_default_config['SSH_STRICT_HOST_KEY_CHECKING'])
145
+
143
146
  ERROR_MESSAGES_TO_IGNORE = __configs_from_file.get('ERROR_MESSAGES_TO_IGNORE', __build_in_default_config['ERROR_MESSAGES_TO_IGNORE'])
144
147
 
145
148
  _DEFAULT_CALLED = __configs_from_file.get('_DEFAULT_CALLED', __build_in_default_config['_DEFAULT_CALLED'])
@@ -187,7 +190,11 @@ class Host:
187
190
 
188
191
  __wildCharacters = ['*','?','x']
189
192
 
190
- __gloablUnavailableHosts = set()
193
+ _no_env = DEFAULT_NO_ENV
194
+
195
+ _env_file = DEFAULT_ENV_FILE
196
+
197
+ __globalUnavailableHosts = set()
191
198
 
192
199
  __ipmiiInterfaceIPPrefix = DEFAULT_IPMI_INTERFACE_IP_PREFIX
193
200
 
@@ -197,7 +204,6 @@ _emo = False
197
204
 
198
205
  _etc_hosts = __configs_from_file.get('_etc_hosts', __build_in_default_config['_etc_hosts'])
199
206
 
200
- _env_file = DEFAULT_ENV_FILE
201
207
 
202
208
  # check if command sshpass is available
203
209
  _binPaths = {}
@@ -265,37 +271,6 @@ def expandIPv4Address(hosts):
265
271
  expandedHosts.extend(expandedHost)
266
272
  return expandedHosts
267
273
 
268
- @cache_decorator
269
- def readEnvFromFile(environemnt_file = ''):
270
- '''
271
- Read the environment variables from env_file
272
- Returns:
273
- dict: A dictionary of environment variables
274
- '''
275
- global env
276
- try:
277
- if env:
278
- return env
279
- except:
280
- env = {}
281
- global _env_file
282
- if environemnt_file:
283
- envf = environemnt_file
284
- else:
285
- envf = _env_file if _env_file else DEFAULT_ENV_FILE
286
- if os.path.exists(envf):
287
- with open(envf,'r') as f:
288
- for line in f:
289
- if line.startswith('#') or not line.strip():
290
- continue
291
- key, value = line.replace('export ', '', 1).strip().split('=', 1)
292
- key = key.strip().strip('"').strip("'")
293
- value = value.strip().strip('"').strip("'")
294
- # avoid infinite recursion
295
- if key != value:
296
- env[key] = value.strip('"').strip("'")
297
- return env
298
-
299
274
  @cache_decorator
300
275
  def getIP(hostname,local=False):
301
276
  '''
@@ -336,9 +311,40 @@ def getIP(hostname,local=False):
336
311
  return socket.gethostbyname(hostname)
337
312
  except:
338
313
  return None
314
+
315
+ @cache_decorator
316
+ def readEnvFromFile(environemnt_file = ''):
317
+ '''
318
+ Read the environment variables from env_file
319
+ Returns:
320
+ dict: A dictionary of environment variables
321
+ '''
322
+ global env
323
+ try:
324
+ if env:
325
+ return env
326
+ except:
327
+ env = {}
328
+ global _env_file
329
+ if environemnt_file:
330
+ envf = environemnt_file
331
+ else:
332
+ envf = _env_file if _env_file else DEFAULT_ENV_FILE
333
+ if os.path.exists(envf):
334
+ with open(envf,'r') as f:
335
+ for line in f:
336
+ if line.startswith('#') or not line.strip():
337
+ continue
338
+ key, value = line.replace('export ', '', 1).strip().split('=', 1)
339
+ key = key.strip().strip('"').strip("'")
340
+ value = value.strip().strip('"').strip("'")
341
+ # avoid infinite recursion
342
+ if key != value:
343
+ env[key] = value.strip('"').strip("'")
344
+ return env
339
345
 
340
346
  @cache_decorator
341
- def expand_hostname(text,validate=True,no_env=False):
347
+ def expand_hostname(text,validate=True):
342
348
  '''
343
349
  Expand the hostname range in the text.
344
350
  Will search the string for a range ( [] encloused and non enclosed number ranges).
@@ -359,12 +365,12 @@ def expand_hostname(text,validate=True,no_env=False):
359
365
  hostname = expandinghosts.pop()
360
366
  match = re.search(r'\[(.*?-.*?)\]', hostname)
361
367
  if not match:
362
- expandedhosts.update(validate_expand_hostname(hostname,no_env=no_env) if validate else [hostname])
368
+ expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
363
369
  continue
364
370
  try:
365
371
  range_start, range_end = match.group(1).split('-')
366
372
  except ValueError:
367
- expandedhosts.update(validate_expand_hostname(hostname,no_env=no_env) if validate else [hostname])
373
+ expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
368
374
  continue
369
375
  range_start = range_start.strip()
370
376
  range_end = range_end.strip()
@@ -376,7 +382,7 @@ def expand_hostname(text,validate=True,no_env=False):
376
382
  elif range_start.isalpha() and range_start.isupper():
377
383
  range_end = 'Z'
378
384
  else:
379
- expandedhosts.update(validate_expand_hostname(hostname,no_env=no_env) if validate else [hostname])
385
+ expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
380
386
  continue
381
387
  if not range_start:
382
388
  if range_end.isdigit():
@@ -386,7 +392,7 @@ def expand_hostname(text,validate=True,no_env=False):
386
392
  elif range_end.isalpha() and range_end.isupper():
387
393
  range_start = 'A'
388
394
  else:
389
- expandedhosts.update(validate_expand_hostname(hostname,no_env=no_env) if validate else [hostname])
395
+ expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
390
396
  continue
391
397
  if range_start.isdigit() and range_end.isdigit():
392
398
  padding_length = min(len(range_start), len(range_end))
@@ -396,14 +402,14 @@ def expand_hostname(text,validate=True,no_env=False):
396
402
  if '[' in hostname:
397
403
  expandinghosts.append(hostname.replace(match.group(0), formatted_i, 1))
398
404
  else:
399
- expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), formatted_i, 1),no_env=no_env) if validate else [hostname])
405
+ expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), formatted_i, 1)) if validate else [hostname])
400
406
  else:
401
407
  if all(c in string.hexdigits for c in range_start + range_end):
402
408
  for i in range(int(range_start, 16), int(range_end, 16)+1):
403
409
  if '[' in hostname:
404
410
  expandinghosts.append(hostname.replace(match.group(0), format(i, 'x'), 1))
405
411
  else:
406
- expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), format(i, 'x'), 1),no_env=no_env) if validate else [hostname])
412
+ expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), format(i, 'x'), 1)) if validate else [hostname])
407
413
  else:
408
414
  try:
409
415
  start_index = alphanumeric.index(range_start)
@@ -412,13 +418,13 @@ def expand_hostname(text,validate=True,no_env=False):
412
418
  if '[' in hostname:
413
419
  expandinghosts.append(hostname.replace(match.group(0), alphanumeric[i], 1))
414
420
  else:
415
- expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), alphanumeric[i], 1),no_env=no_env) if validate else [hostname])
421
+ expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), alphanumeric[i], 1)) if validate else [hostname])
416
422
  except ValueError:
417
- expandedhosts.update(validate_expand_hostname(hostname,no_env=no_env) if validate else [hostname])
423
+ expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
418
424
  return expandedhosts
419
425
 
420
426
  @cache_decorator
421
- def expand_hostnames(hosts,no_env=False):
427
+ def expand_hostnames(hosts):
422
428
  '''
423
429
  Expand the hostnames in the hosts list
424
430
 
@@ -447,17 +453,17 @@ def expand_hostnames(hosts,no_env=False):
447
453
  if re.match(r'^((((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?))?)|(\[((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])}|x|\*|\?))?\]))(\.((((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|x|\*|\?)(-((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|x|\*|\?))?)|(\[((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|x|\*|\?)(-((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|x|\*|\?))?\]))){2}(\.(((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?))?)|(\[((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])|x|\*|\?)(-((25[0-4]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])}|x|\*|\?))?\]))$', host):
448
454
  hostSetToAdd = sorted(expandIPv4Address(frozenset([host])),key=ipaddress.IPv4Address)
449
455
  else:
450
- hostSetToAdd = sorted(expand_hostname(host,no_env=no_env))
456
+ hostSetToAdd = sorted(expand_hostname(host))
451
457
  if username:
452
458
  # we expand the username
453
- username = sorted(expand_hostname(username,validate=False,no_env=no_env))
459
+ username = sorted(expand_hostname(username,validate=False))
454
460
  # we combine the username and hostname
455
461
  hostSetToAdd = [u+'@'+h for u,h in product(username,hostSetToAdd)]
456
462
  expandedhosts.extend(hostSetToAdd)
457
463
  return expandedhosts
458
464
 
459
465
  @cache_decorator
460
- def validate_expand_hostname(hostname,no_env=False):
466
+ def validate_expand_hostname(hostname):
461
467
  '''
462
468
  Validate the hostname and expand it if it is a range of IP addresses
463
469
 
@@ -467,17 +473,18 @@ def validate_expand_hostname(hostname,no_env=False):
467
473
  Returns:
468
474
  list: A list of valid hostnames
469
475
  '''
476
+ global _no_env
470
477
  # maybe it is just defined in ./target_files/hosts.sh and exported to the environment
471
478
  # we will try to get the valid host name from the environment
472
479
  hostname = hostname.strip('$')
473
480
  if getIP(hostname,local=True):
474
481
  return [hostname]
475
- elif not no_env and hostname in os.environ:
482
+ elif not _no_env and hostname in os.environ:
476
483
  # we will expand these hostnames again
477
- return expand_hostnames(frozenset(os.environ[hostname].split(',')),no_env=no_env)
484
+ return expand_hostnames(frozenset(os.environ[hostname].split(',')))
478
485
  elif hostname in readEnvFromFile():
479
486
  # we will expand these hostnames again
480
- return expand_hostnames(frozenset(readEnvFromFile()[hostname].split(',')),no_env=no_env)
487
+ return expand_hostnames(frozenset(readEnvFromFile()[hostname].split(',')))
481
488
  elif getIP(hostname,local=False):
482
489
  return [hostname]
483
490
  else:
@@ -607,6 +614,11 @@ def ssh_command(host, sem, timeout=60,passwds=None):
607
614
  global __ipmiiInterfaceIPPrefix
608
615
  global _binPaths
609
616
  try:
617
+ keyCheckArgs = []
618
+ rsyncKeyCheckArgs = []
619
+ if not SSH_STRICT_HOST_KEY_CHECKING:
620
+ keyCheckArgs = ['-o StrictHostKeyChecking=no','-o UserKnownHostsFile=/dev/null']
621
+ rsyncKeyCheckArgs = ['--rsh','ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null']
610
622
  host.username = None
611
623
  host.address = host.name
612
624
  if '@' in host.name:
@@ -697,11 +709,11 @@ def ssh_command(host, sem, timeout=60,passwds=None):
697
709
  else:
698
710
  fileArgs = host.files + [f'{host.resolvedName}:{host.command}']
699
711
  if useScp:
700
- formatedCMD = [_binPaths['scp'],'-rpB'] + extraargs +['--']+fileArgs
712
+ formatedCMD = [_binPaths['scp'],'-rpB'] + keyCheckArgs + extraargs +['--']+fileArgs
701
713
  else:
702
- formatedCMD = [_binPaths['rsync'],'-ahlX','--partial','--inplace', '--info=name'] + extraargs +['--']+fileArgs
714
+ formatedCMD = [_binPaths['rsync'],'-ahlX','--partial','--inplace', '--info=name'] + rsyncKeyCheckArgs + extraargs +['--']+fileArgs
703
715
  else:
704
- formatedCMD = [_binPaths['ssh']] + extraargs +['--']+ [host.resolvedName, host.command]
716
+ formatedCMD = [_binPaths['ssh']] + keyCheckArgs + extraargs +['--']+ [host.resolvedName, host.command]
705
717
  if passwds and 'sshpass' in _binPaths:
706
718
  formatedCMD = [_binPaths['sshpass'], '-p', passwds] + formatedCMD
707
719
  elif passwds:
@@ -1226,34 +1238,34 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
1226
1238
  print(rtnStr)
1227
1239
  return rtnStr
1228
1240
 
1229
- sshConfigged = False
1230
- def verify_ssh_config():
1231
- '''
1232
- Verify that ~/.ssh/config exists and contains the line "StrictHostKeyChecking no"
1233
-
1234
- Args:
1235
- None
1236
-
1237
- Returns:
1238
- None
1239
- '''
1240
- global sshConfigged
1241
- if not sshConfigged:
1242
- # first we make sure ~/.ssh/config exists
1243
- config = ''
1244
- if not os.path.exists(os.path.expanduser('~/.ssh')):
1245
- os.makedirs(os.path.expanduser('~/.ssh'))
1246
- if os.path.exists(os.path.expanduser('~/.ssh/config')):
1247
- with open(os.path.expanduser('~/.ssh/config'),'r') as f:
1248
- config = f.read()
1249
- if config:
1250
- if 'StrictHostKeyChecking no' not in config:
1251
- with open(os.path.expanduser('~/.ssh/config'),'a') as f:
1252
- f.write('\nHost *\n\tStrictHostKeyChecking no\n')
1253
- else:
1254
- with open(os.path.expanduser('~/.ssh/config'),'w') as f:
1255
- f.write('Host *\n\tStrictHostKeyChecking no\n')
1256
- sshConfigged = True
1241
+ # sshConfigged = False
1242
+ # def verify_ssh_config():
1243
+ # '''
1244
+ # Verify that ~/.ssh/config exists and contains the line "StrictHostKeyChecking no"
1245
+
1246
+ # Args:
1247
+ # None
1248
+
1249
+ # Returns:
1250
+ # None
1251
+ # '''
1252
+ # global sshConfigged
1253
+ # if not sshConfigged:
1254
+ # # first we make sure ~/.ssh/config exists
1255
+ # config = ''
1256
+ # if not os.path.exists(os.path.expanduser('~/.ssh')):
1257
+ # os.makedirs(os.path.expanduser('~/.ssh'))
1258
+ # if os.path.exists(os.path.expanduser('~/.ssh/config')):
1259
+ # with open(os.path.expanduser('~/.ssh/config'),'r') as f:
1260
+ # config = f.read()
1261
+ # if config:
1262
+ # if 'StrictHostKeyChecking no' not in config:
1263
+ # with open(os.path.expanduser('~/.ssh/config'),'a') as f:
1264
+ # f.write('\nHost *\n\tStrictHostKeyChecking no\n')
1265
+ # else:
1266
+ # with open(os.path.expanduser('~/.ssh/config'),'w') as f:
1267
+ # f.write('Host *\n\tStrictHostKeyChecking no\n')
1268
+ # sshConfigged = True
1257
1269
 
1258
1270
  def signal_handler(sig, frame):
1259
1271
  '''
@@ -1277,9 +1289,9 @@ def signal_handler(sig, frame):
1277
1289
  os.system(f'pkill -ef {os.path.basename(__file__)}')
1278
1290
  sys.exit(0)
1279
1291
 
1280
-
1281
1292
  def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, nowatch, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window = DEFAULT_SINGLE_WINDOW):
1282
- global __gloablUnavailableHosts
1293
+ global __globalUnavailableHosts
1294
+ global _no_env
1283
1295
  threads = start_run_on_hosts(hosts, timeout=timeout,password=password,max_connections=max_connections)
1284
1296
  if not nowatch and threads and not returnUnfinished and any([thread.is_alive() for thread in threads]) and sys.stdout.isatty() and os.get_terminal_size() and os.get_terminal_size().columns > 10:
1285
1297
  curses.wrapper(curses_print, hosts, threads, min_char_len = curses_min_char_len, min_line_len = curses_min_line_len, single_window = single_window)
@@ -1292,7 +1304,11 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
1292
1304
  # update the unavailable hosts and global unavailable hosts
1293
1305
  if willUpdateUnreachableHosts:
1294
1306
  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!'))])
1295
- __gloablUnavailableHosts.update(unavailableHosts)
1307
+ __globalUnavailableHosts.update(unavailableHosts)
1308
+ # update the os environment variable if not _no_env
1309
+ if not _no_env:
1310
+ os.environ['__multiSSH3_UNAVAILABLE_HOSTS'] = ','.join(unavailableHosts)
1311
+
1296
1312
  # print the output, if the output of multiple hosts are the same, we aggragate them
1297
1313
  if not called:
1298
1314
  print_output(hosts,json,greppable=greppable)
@@ -1394,6 +1410,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1394
1410
  interface_ip_prefix (str, optional): The prefix of the IPMI interface. Defaults to {DEFAULT_INTERFACE_IP_PREFIX}.
1395
1411
  returnUnfinished (bool, optional): Whether to return the unfinished hosts. Defaults to {_DEFAULT_RETURN_UNFINISHED}.
1396
1412
  scp (bool, optional): Whether to use scp instead of rsync. Defaults to {DEFAULT_SCP}.
1413
+ gather_mode (bool, optional): Whether to use gather mode. Defaults to False.
1397
1414
  username (str, optional): The username to use to connect to the hosts. Defaults to {DEFAULT_USERNAME}.
1398
1415
  extraargs (str, optional): Extra arguments to pass to the ssh / rsync / scp command. Defaults to {DEFAULT_EXTRA_ARGS}.
1399
1416
  skipUnreachable (bool, optional): Whether to skip unreachable hosts. Defaults to {DEFAULT_SKIP_UNREACHABLE}.
@@ -1410,8 +1427,16 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1410
1427
  Returns:
1411
1428
  list: A list of Host objects
1412
1429
  '''
1413
- global __gloablUnavailableHosts
1430
+ global __globalUnavailableHosts
1414
1431
  global __global_suppress_printout
1432
+ global _no_env
1433
+ global _emo
1434
+ _emo = False
1435
+ _no_env = no_env
1436
+ if not no_env and '__multiSSH3_UNAVAILABLE_HOSTS' in os.environ:
1437
+ __globalUnavailableHosts = set(os.environ['__multiSSH3_UNAVAILABLE_HOSTS'].split(','))
1438
+ elif '__multiSSH3_UNAVAILABLE_HOSTS' in readEnvFromFile():
1439
+ __globalUnavailableHosts = set(readEnvFromFile()['__multiSSH3_UNAVAILABLE_HOSTS'].split(','))
1415
1440
  if not max_connections:
1416
1441
  max_connections = 4 * os.cpu_count()
1417
1442
  elif max_connections == 0:
@@ -1420,7 +1445,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1420
1445
  max_connections = (-max_connections) * os.cpu_count()
1421
1446
  if not commands:
1422
1447
  commands = []
1423
- verify_ssh_config()
1448
+ #verify_ssh_config()
1424
1449
  # load global unavailable hosts only if the function is called (so using --repeat will not load the unavailable hosts again)
1425
1450
  if called:
1426
1451
  # if called,
@@ -1429,18 +1454,17 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1429
1454
  if skipUnreachable is None:
1430
1455
  skipUnreachable = True
1431
1456
  if skipUnreachable:
1432
- unavailableHosts = __gloablUnavailableHosts
1457
+ unavailableHosts = __globalUnavailableHosts
1433
1458
  else:
1434
1459
  unavailableHosts = set()
1435
1460
  else:
1436
1461
  # if run in command line ( or emulating running in command line, we default to skip unreachable hosts within one command call )
1437
1462
  if skipUnreachable:
1438
- unavailableHosts = __gloablUnavailableHosts
1463
+ unavailableHosts = __globalUnavailableHosts
1439
1464
  else:
1440
1465
  unavailableHosts = set()
1441
1466
  skipUnreachable = True
1442
- global _emo
1443
- _emo = False
1467
+
1444
1468
  # We create the hosts
1445
1469
  hostStr = formHostStr(hosts)
1446
1470
  skipHostStr = formHostStr(skip_hosts) if skip_hosts else ''
@@ -1459,8 +1483,8 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1459
1483
  if '@' not in host:
1460
1484
  skipHostStr[i] = userStr + host
1461
1485
  skipHostStr = ','.join(skipHostStr)
1462
- targetHostsList = expand_hostnames(frozenset(hostStr.split(',')),no_env=no_env)
1463
- skipHostsList = expand_hostnames(frozenset(skipHostStr.split(',')),no_env=no_env)
1486
+ targetHostsList = expand_hostnames(frozenset(hostStr.split(',')))
1487
+ skipHostsList = expand_hostnames(frozenset(skipHostStr.split(',')))
1464
1488
  if skipHostsList:
1465
1489
  if not __global_suppress_printout: print(f"Skipping hosts: {skipHostsList}")
1466
1490
  if files and not commands:
@@ -1471,15 +1495,18 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1471
1495
  files = set(files+commands) if files else set(commands)
1472
1496
  if files:
1473
1497
  # try to resolve files first (like * etc)
1474
- pathSet = set()
1475
- for file in files:
1476
- try:
1477
- pathSet.update(glob.glob(file,include_hidden=True,recursive=True))
1478
- except:
1479
- pathSet.update(glob.glob(file,recursive=True))
1480
- if not pathSet:
1481
- print(f'Warning: No source files at {files} are found after resolving globs!')
1482
- sys.exit(66)
1498
+ if not gather_mode:
1499
+ pathSet = set()
1500
+ for file in files:
1501
+ try:
1502
+ pathSet.update(glob.glob(file,include_hidden=True,recursive=True))
1503
+ except:
1504
+ pathSet.update(glob.glob(file,recursive=True))
1505
+ if not pathSet:
1506
+ print(f'Warning: No source files at {files} are found after resolving globs!')
1507
+ sys.exit(66)
1508
+ else:
1509
+ pathSet = set(files)
1483
1510
  if file_sync:
1484
1511
  # use abosolute path for file sync
1485
1512
  commands = [os.path.abspath(file) for file in pathSet]
@@ -1598,6 +1625,7 @@ def get_default_config(args):
1598
1625
  'DEFAULT_GREPPABLE_MODE': args.greppable,
1599
1626
  'DEFAULT_SKIP_UNREACHABLE': args.skip_unreachable,
1600
1627
  'DEFAULT_SKIP_HOSTS': args.skip_hosts,
1628
+ 'SSH_STRICT_HOST_KEY_CHECKING': SSH_STRICT_HOST_KEY_CHECKING,
1601
1629
  'ERROR_MESSAGES_TO_IGNORE': ERROR_MESSAGES_TO_IGNORE,
1602
1630
  }
1603
1631
 
@@ -1605,14 +1633,15 @@ def write_default_config(args,CONFIG_FILE,backup = True):
1605
1633
  if backup and os.path.exists(CONFIG_FILE):
1606
1634
  os.rename(CONFIG_FILE,CONFIG_FILE+'.bak')
1607
1635
  default_config = get_default_config(args)
1636
+ # apply the updated defualt_config to __configs_from_file and write that to file
1637
+ __configs_from_file.update(default_config)
1608
1638
  with open(CONFIG_FILE,'w') as f:
1609
- json.dump(default_config,f,indent=4)
1639
+ json.dump(__configs_from_file,f,indent=4)
1610
1640
 
1611
1641
 
1612
1642
  def main():
1613
1643
  global _emo
1614
1644
  global __global_suppress_printout
1615
- global __gloablUnavailableHosts
1616
1645
  global __mainReturnCode
1617
1646
  global __failedHosts
1618
1647
  global __ipmiiInterfaceIPPrefix
@@ -1646,22 +1675,22 @@ def main():
1646
1675
  parser.add_argument('-sw','--single_window', action='store_true', help=f'Use a single window for all hosts. (default: {DEFAULT_SINGLE_WINDOW})', default=DEFAULT_SINGLE_WINDOW)
1647
1676
  parser.add_argument('-eo','--error_only', action='store_true', help=f'Only print the error output. (default: {DEFAULT_ERROR_ONLY})', default=DEFAULT_ERROR_ONLY)
1648
1677
  parser.add_argument("-no","--no_output", action='store_true', help=f"Do not print the output. (default: {DEFAULT_NO_OUTPUT})", default=DEFAULT_NO_OUTPUT)
1649
- parser.add_argument('--no_env', action='store_true', help=f'Do not load the environment variables. (default: {DEFAULT_NO_ENV})', default=DEFAULT_NO_ENV)
1650
- parser.add_argument("--env_file", type=str, help=f"The file to load the environment variables from. (default: {DEFAULT_ENV_FILE})", default=DEFAULT_ENV_FILE)
1678
+ parser.add_argument('--no_env', action='store_true', help=f'Do not load the command line environment variables. (default: {DEFAULT_NO_ENV})', default=DEFAULT_NO_ENV)
1679
+ parser.add_argument("--env_file", type=str, help=f"The file to load the mssh file based environment variables from. ( Still work with --no_env ) (default: {DEFAULT_ENV_FILE})", default=DEFAULT_ENV_FILE)
1651
1680
  parser.add_argument("-m","--max_connections", type=int, help=f"Max number of connections to use (default: 4 * cpu_count)", default=DEFAULT_MAX_CONNECTIONS)
1652
1681
  parser.add_argument("-j","--json", action='store_true', help=F"Output in json format. (default: {DEFAULT_JSON_MODE})", default=DEFAULT_JSON_MODE)
1653
1682
  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)
1654
1683
  parser.add_argument("-g","--greppable", action='store_true', help=f"Output in greppable format. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
1655
1684
  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)
1656
1685
  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)
1657
- parser.add_argument('--generate_default_config_file', action='store_true', help=f'Generate / store the default config file from command line argument and current config at {CONFIG_FILE}')
1686
+ 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}')
1658
1687
  parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} with [ {", ".join(_binPaths.keys())} ] by {AUTHOR} ({AUTHOR_EMAIL})')
1659
1688
 
1660
1689
  # parser.add_argument('-u', '--user', metavar='user', type=str, nargs=1,
1661
1690
  # help='the user to use to connect to the hosts')
1662
1691
  args = parser.parse_args()
1663
1692
 
1664
- if args.generate_default_config_file:
1693
+ if args.store_config_file:
1665
1694
  try:
1666
1695
  if os.path.exists(CONFIG_FILE):
1667
1696
  print(f"Warning: {CONFIG_FILE} already exists, what to do? (o/b/n)")
@@ -1681,6 +1710,8 @@ def main():
1681
1710
  except Exception as e:
1682
1711
  print(f"Error while writing config file: {e}")
1683
1712
  if not args.commands:
1713
+ with open(CONFIG_FILE,'r') as f:
1714
+ print(f"Config file content: \n{f.read()}")
1684
1715
  sys.exit(0)
1685
1716
 
1686
1717
  _env_file = args.env_file
@@ -1,7 +0,0 @@
1
- multiSSH3.py,sha256=1Un4-afx2wCyXWKgy1-Y55fpth0_L8zdb-qa_Cm3U-s,85689
2
- multiSSH3-4.83.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
3
- multiSSH3-4.83.dist-info/METADATA,sha256=A3OVJ44Q0Oi8_1na-AredxReylobi6DX2m87o1z7-4s,15887
4
- multiSSH3-4.83.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
5
- multiSSH3-4.83.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
6
- multiSSH3-4.83.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
7
- multiSSH3-4.83.dist-info/RECORD,,