multiSSH3 4.83__py3-none-any.whl → 4.92__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.92
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=XGiNVv-ZsYMGFVrZ8zpeK4wz170Fi8YRtltcRxrQQCI,88186
2
+ multiSSH3-4.92.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
3
+ multiSSH3-4.92.dist-info/METADATA,sha256=lesBhivYV4dahRGYUWvo-jbxcOEhEl6VHhSjQ1U-7-M,16043
4
+ multiSSH3-4.92.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
5
+ multiSSH3-4.92.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
6
+ multiSSH3-4.92.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
7
+ multiSSH3-4.92.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.92'
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'])
@@ -170,6 +173,7 @@ class Host:
170
173
  self.stdout = [] # the stdout of the command
171
174
  self.stderr = [] # the stderr of the command
172
175
  self.printedLines = -1 # the number of lines printed on the screen
176
+ self.lastUpdateTime = time.time() # the last time the output was updated
173
177
  self.files = files # the files to be copied to the host
174
178
  self.ipmi = ipmi # whether to use ipmi to connect to the host
175
179
  self.interface_ip_prefix = interface_ip_prefix # the prefix of the ip address of the interface to be used to connect to the host
@@ -187,7 +191,11 @@ class Host:
187
191
 
188
192
  __wildCharacters = ['*','?','x']
189
193
 
190
- __gloablUnavailableHosts = set()
194
+ _no_env = DEFAULT_NO_ENV
195
+
196
+ _env_file = DEFAULT_ENV_FILE
197
+
198
+ __globalUnavailableHosts = set()
191
199
 
192
200
  __ipmiiInterfaceIPPrefix = DEFAULT_IPMI_INTERFACE_IP_PREFIX
193
201
 
@@ -197,7 +205,6 @@ _emo = False
197
205
 
198
206
  _etc_hosts = __configs_from_file.get('_etc_hosts', __build_in_default_config['_etc_hosts'])
199
207
 
200
- _env_file = DEFAULT_ENV_FILE
201
208
 
202
209
  # check if command sshpass is available
203
210
  _binPaths = {}
@@ -265,37 +272,6 @@ def expandIPv4Address(hosts):
265
272
  expandedHosts.extend(expandedHost)
266
273
  return expandedHosts
267
274
 
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
275
  @cache_decorator
300
276
  def getIP(hostname,local=False):
301
277
  '''
@@ -336,9 +312,40 @@ def getIP(hostname,local=False):
336
312
  return socket.gethostbyname(hostname)
337
313
  except:
338
314
  return None
315
+
316
+ @cache_decorator
317
+ def readEnvFromFile(environemnt_file = ''):
318
+ '''
319
+ Read the environment variables from env_file
320
+ Returns:
321
+ dict: A dictionary of environment variables
322
+ '''
323
+ global env
324
+ try:
325
+ if env:
326
+ return env
327
+ except:
328
+ env = {}
329
+ global _env_file
330
+ if environemnt_file:
331
+ envf = environemnt_file
332
+ else:
333
+ envf = _env_file if _env_file else DEFAULT_ENV_FILE
334
+ if os.path.exists(envf):
335
+ with open(envf,'r') as f:
336
+ for line in f:
337
+ if line.startswith('#') or not line.strip():
338
+ continue
339
+ key, value = line.replace('export ', '', 1).strip().split('=', 1)
340
+ key = key.strip().strip('"').strip("'")
341
+ value = value.strip().strip('"').strip("'")
342
+ # avoid infinite recursion
343
+ if key != value:
344
+ env[key] = value.strip('"').strip("'")
345
+ return env
339
346
 
340
347
  @cache_decorator
341
- def expand_hostname(text,validate=True,no_env=False):
348
+ def expand_hostname(text,validate=True):
342
349
  '''
343
350
  Expand the hostname range in the text.
344
351
  Will search the string for a range ( [] encloused and non enclosed number ranges).
@@ -359,12 +366,12 @@ def expand_hostname(text,validate=True,no_env=False):
359
366
  hostname = expandinghosts.pop()
360
367
  match = re.search(r'\[(.*?-.*?)\]', hostname)
361
368
  if not match:
362
- expandedhosts.update(validate_expand_hostname(hostname,no_env=no_env) if validate else [hostname])
369
+ expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
363
370
  continue
364
371
  try:
365
372
  range_start, range_end = match.group(1).split('-')
366
373
  except ValueError:
367
- expandedhosts.update(validate_expand_hostname(hostname,no_env=no_env) if validate else [hostname])
374
+ expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
368
375
  continue
369
376
  range_start = range_start.strip()
370
377
  range_end = range_end.strip()
@@ -376,7 +383,7 @@ def expand_hostname(text,validate=True,no_env=False):
376
383
  elif range_start.isalpha() and range_start.isupper():
377
384
  range_end = 'Z'
378
385
  else:
379
- expandedhosts.update(validate_expand_hostname(hostname,no_env=no_env) if validate else [hostname])
386
+ expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
380
387
  continue
381
388
  if not range_start:
382
389
  if range_end.isdigit():
@@ -386,7 +393,7 @@ def expand_hostname(text,validate=True,no_env=False):
386
393
  elif range_end.isalpha() and range_end.isupper():
387
394
  range_start = 'A'
388
395
  else:
389
- expandedhosts.update(validate_expand_hostname(hostname,no_env=no_env) if validate else [hostname])
396
+ expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
390
397
  continue
391
398
  if range_start.isdigit() and range_end.isdigit():
392
399
  padding_length = min(len(range_start), len(range_end))
@@ -396,14 +403,14 @@ def expand_hostname(text,validate=True,no_env=False):
396
403
  if '[' in hostname:
397
404
  expandinghosts.append(hostname.replace(match.group(0), formatted_i, 1))
398
405
  else:
399
- expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), formatted_i, 1),no_env=no_env) if validate else [hostname])
406
+ expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), formatted_i, 1)) if validate else [hostname])
400
407
  else:
401
408
  if all(c in string.hexdigits for c in range_start + range_end):
402
409
  for i in range(int(range_start, 16), int(range_end, 16)+1):
403
410
  if '[' in hostname:
404
411
  expandinghosts.append(hostname.replace(match.group(0), format(i, 'x'), 1))
405
412
  else:
406
- expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), format(i, 'x'), 1),no_env=no_env) if validate else [hostname])
413
+ expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), format(i, 'x'), 1)) if validate else [hostname])
407
414
  else:
408
415
  try:
409
416
  start_index = alphanumeric.index(range_start)
@@ -412,13 +419,13 @@ def expand_hostname(text,validate=True,no_env=False):
412
419
  if '[' in hostname:
413
420
  expandinghosts.append(hostname.replace(match.group(0), alphanumeric[i], 1))
414
421
  else:
415
- expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), alphanumeric[i], 1),no_env=no_env) if validate else [hostname])
422
+ expandedhosts.update(validate_expand_hostname(hostname.replace(match.group(0), alphanumeric[i], 1)) if validate else [hostname])
416
423
  except ValueError:
417
- expandedhosts.update(validate_expand_hostname(hostname,no_env=no_env) if validate else [hostname])
424
+ expandedhosts.update(validate_expand_hostname(hostname) if validate else [hostname])
418
425
  return expandedhosts
419
426
 
420
427
  @cache_decorator
421
- def expand_hostnames(hosts,no_env=False):
428
+ def expand_hostnames(hosts):
422
429
  '''
423
430
  Expand the hostnames in the hosts list
424
431
 
@@ -447,17 +454,17 @@ def expand_hostnames(hosts,no_env=False):
447
454
  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
455
  hostSetToAdd = sorted(expandIPv4Address(frozenset([host])),key=ipaddress.IPv4Address)
449
456
  else:
450
- hostSetToAdd = sorted(expand_hostname(host,no_env=no_env))
457
+ hostSetToAdd = sorted(expand_hostname(host))
451
458
  if username:
452
459
  # we expand the username
453
- username = sorted(expand_hostname(username,validate=False,no_env=no_env))
460
+ username = sorted(expand_hostname(username,validate=False))
454
461
  # we combine the username and hostname
455
462
  hostSetToAdd = [u+'@'+h for u,h in product(username,hostSetToAdd)]
456
463
  expandedhosts.extend(hostSetToAdd)
457
464
  return expandedhosts
458
465
 
459
466
  @cache_decorator
460
- def validate_expand_hostname(hostname,no_env=False):
467
+ def validate_expand_hostname(hostname):
461
468
  '''
462
469
  Validate the hostname and expand it if it is a range of IP addresses
463
470
 
@@ -467,17 +474,18 @@ def validate_expand_hostname(hostname,no_env=False):
467
474
  Returns:
468
475
  list: A list of valid hostnames
469
476
  '''
477
+ global _no_env
470
478
  # maybe it is just defined in ./target_files/hosts.sh and exported to the environment
471
479
  # we will try to get the valid host name from the environment
472
480
  hostname = hostname.strip('$')
473
481
  if getIP(hostname,local=True):
474
482
  return [hostname]
475
- elif not no_env and hostname in os.environ:
483
+ elif not _no_env and hostname in os.environ:
476
484
  # we will expand these hostnames again
477
- return expand_hostnames(frozenset(os.environ[hostname].split(',')),no_env=no_env)
485
+ return expand_hostnames(frozenset(os.environ[hostname].split(',')))
478
486
  elif hostname in readEnvFromFile():
479
487
  # we will expand these hostnames again
480
- return expand_hostnames(frozenset(readEnvFromFile()[hostname].split(',')),no_env=no_env)
488
+ return expand_hostnames(frozenset(readEnvFromFile()[hostname].split(',')))
481
489
  elif getIP(hostname,local=False):
482
490
  return [hostname]
483
491
  else:
@@ -534,6 +542,7 @@ def handle_reading_stream(stream,target, host):
534
542
  current_line_str = current_line.decode('utf-8',errors='backslashreplace')
535
543
  target.append(current_line_str)
536
544
  host.output.append(current_line_str)
545
+ host.lastUpdateTime = time.time()
537
546
  current_line = bytearray()
538
547
  lastLineCommited = True
539
548
  for char in iter(lambda:stream.read(1), b''):
@@ -577,6 +586,7 @@ def handle_writing_stream(stream,stop_event,host):
577
586
  host.output.append(' $ ' + ''.join(__keyPressesIn[sentInput]).encode().decode().replace('\n', '↵'))
578
587
  host.stdout.append(' $ ' + ''.join(__keyPressesIn[sentInput]).encode().decode().replace('\n', '↵'))
579
588
  sentInput += 1
589
+ host.lastUpdateTime = time.time()
580
590
  else:
581
591
  time.sleep(0.1)
582
592
  if sentInput < len(__keyPressesIn) - 1 :
@@ -607,6 +617,11 @@ def ssh_command(host, sem, timeout=60,passwds=None):
607
617
  global __ipmiiInterfaceIPPrefix
608
618
  global _binPaths
609
619
  try:
620
+ keyCheckArgs = []
621
+ rsyncKeyCheckArgs = []
622
+ if not SSH_STRICT_HOST_KEY_CHECKING:
623
+ keyCheckArgs = ['-o StrictHostKeyChecking=no','-o UserKnownHostsFile=/dev/null']
624
+ rsyncKeyCheckArgs = ['--rsh','ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null']
610
625
  host.username = None
611
626
  host.address = host.name
612
627
  if '@' in host.name:
@@ -697,11 +712,11 @@ def ssh_command(host, sem, timeout=60,passwds=None):
697
712
  else:
698
713
  fileArgs = host.files + [f'{host.resolvedName}:{host.command}']
699
714
  if useScp:
700
- formatedCMD = [_binPaths['scp'],'-rpB'] + extraargs +['--']+fileArgs
715
+ formatedCMD = [_binPaths['scp'],'-rpB'] + keyCheckArgs + extraargs +['--']+fileArgs
701
716
  else:
702
- formatedCMD = [_binPaths['rsync'],'-ahlX','--partial','--inplace', '--info=name'] + extraargs +['--']+fileArgs
717
+ formatedCMD = [_binPaths['rsync'],'-ahlX','--partial','--inplace', '--info=name'] + rsyncKeyCheckArgs + extraargs +['--']+fileArgs
703
718
  else:
704
- formatedCMD = [_binPaths['ssh']] + extraargs +['--']+ [host.resolvedName, host.command]
719
+ formatedCMD = [_binPaths['ssh']] + keyCheckArgs + extraargs +['--']+ [host.resolvedName, host.command]
705
720
  if passwds and 'sshpass' in _binPaths:
706
721
  formatedCMD = [_binPaths['sshpass'], '-p', passwds] + formatedCMD
707
722
  elif passwds:
@@ -738,14 +753,11 @@ def ssh_command(host, sem, timeout=60,passwds=None):
738
753
  stdin_thread = threading.Thread(target=handle_writing_stream, args=(proc.stdin,stdin_stop_event, host), daemon=True)
739
754
  stdin_thread.start()
740
755
  # Monitor the subprocess and terminate it after the timeout
741
- start_time = time.time()
742
- outLength = len(host.output)
756
+ host.lastUpdateTime = time.time()
757
+ timeoutLineAppended = False
743
758
  while proc.poll() is None: # while the process is still running
744
- if len(host.output) > outLength:
745
- start_time = time.time()
746
- outLength = len(host.output)
747
759
  if timeout > 0:
748
- if time.time() - start_time > timeout:
760
+ if time.time() - host.lastUpdateTime > timeout:
749
761
  host.stderr.append('Timeout!')
750
762
  host.output.append('Timeout!')
751
763
  proc.send_signal(signal.SIGINT)
@@ -753,15 +765,19 @@ def ssh_command(host, sem, timeout=60,passwds=None):
753
765
 
754
766
  proc.terminate()
755
767
  break
756
- elif time.time() - start_time > min(10, timeout // 2):
757
- timeoutLine = f'Timeout in [{timeout - int(time.time() - start_time)}] seconds!'
768
+ elif time.time() - host.lastUpdateTime > min(30, timeout // 2):
769
+ timeoutLine = f'Timeout in [{timeout - int(time.time() - host.lastUpdateTime)}] seconds!'
758
770
  if host.output and not host.output[-1].strip().startswith(timeoutLine):
759
771
  # remove last line if it is a countdown
760
- if host.output and host.output[-1].strip().endswith('] seconds!') and host.output[-1].strip().startswith('Timeout in ['):
772
+ if host.output and timeoutLineAppended and host.output[-1].strip().endswith('] seconds!') and host.output[-1].strip().startswith('Timeout in ['):
761
773
  host.output.pop()
762
774
  host.printedLines -= 1
763
775
  host.output.append(timeoutLine)
764
- outLength = len(host.output)
776
+ timeoutLineAppended = True
777
+ elif host.output and timeoutLineAppended and host.output[-1].strip().endswith('] seconds!') and host.output[-1].strip().startswith('Timeout in ['):
778
+ host.output.pop()
779
+ host.printedLines -= 1
780
+ timeoutLineAppended = False
765
781
  if _emo:
766
782
  host.stderr.append('Ctrl C detected, Emergency Stop!')
767
783
  host.output.append('Ctrl C detected, Emergency Stop!')
@@ -954,7 +970,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
954
970
  bottom_border = None
955
971
  if y + host_window_height < org_dim[0]:
956
972
  bottom_border = curses.newwin(1, max_x, y + host_window_height, 0)
957
- bottom_border.clear()
973
+ #bottom_border.clear()
958
974
  bottom_border.addstr(0, 0, '-' * (max_x - 1))
959
975
  bottom_border.refresh()
960
976
  while host_stats['running'] > 0 or host_stats['waiting'] > 0:
@@ -1047,7 +1063,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
1047
1063
  bottom_stats = '└'+ f"Total: {len(hosts)} Running: {host_stats['running']} Failed: {host_stats['failed']} Finished: {host_stats['finished']} Waiting: {host_stats['waiting']}"[:max_x - 2].center(max_x - 2, "─")
1048
1064
  if bottom_stats != old_bottom_stat:
1049
1065
  old_bottom_stat = bottom_stats
1050
- bottom_border.clear()
1066
+ #bottom_border.clear()
1051
1067
  bottom_border.addstr(0, 0, bottom_stats)
1052
1068
  bottom_border.refresh()
1053
1069
  if stats != old_stat or curserPosition != old_cursor_position:
@@ -1058,7 +1074,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
1058
1074
  curserPositionStats = min(min(curserPosition,len(encodedLine) -1) + stats.find('Send CMD: ')+len('Send CMD: '), max_x -2)
1059
1075
  else:
1060
1076
  curserPositionStats = max_x -2
1061
- stat_window.clear()
1077
+ #stat_window.clear()
1062
1078
  #stat_window.addstr(0, 0, stats)
1063
1079
  # add the line with curser that inverses the color at the curser position
1064
1080
  stat_window.addstr(0, 0, stats[:curserPositionStats], curses.color_pair(1))
@@ -1074,7 +1090,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
1074
1090
  # we will only update the window if there is new output or the window is not fully printed
1075
1091
  if new_configured or host.printedLines < len(host.output):
1076
1092
  try:
1077
- host_window.clear()
1093
+ #host_window.clear()
1078
1094
  # we will try to center the name of the host with ┼ at the beginning and end and ─ in between
1079
1095
  linePrintOut = f'┼{(host.name+":["+host.command+"]")[:host_window_width - 2].center(host_window_width - 1, "─")}'.replace('\n', ' ').replace('\r', ' ').strip()
1080
1096
  host_window.addstr(0, 0, linePrintOut)
@@ -1082,12 +1098,12 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
1082
1098
  for i, line in enumerate(host.output[-(host_window_height - 1):]):
1083
1099
  # print(f"Printng a line at {i + 1} with length of {len('│'+line[:host_window_width - 1])}")
1084
1100
  # time.sleep(10)
1085
- linePrintOut = ('│'+line[:host_window_width - 2].replace('\n', ' ').replace('\r', ' ')).strip()
1101
+ linePrintOut = ('│'+line[:host_window_width - 2].replace('\n', ' ').replace('\r', ' ')).strip().ljust(host_window_width - 1, ' ')
1086
1102
  host_window.addstr(i + 1, 0, linePrintOut)
1087
1103
  # we draw the rest of the available lines
1088
1104
  for i in range(len(host.output), host_window_height - 1):
1089
1105
  # print(f"Printng a line at {i + 1} with length of {len('│')}")
1090
- host_window.addstr(i + 1, 0, '│')
1106
+ host_window.addstr(i + 1, 0, '│'.ljust(host_window_width - 1, ' '))
1091
1107
  host.printedLines = len(host.output)
1092
1108
  host_window.refresh()
1093
1109
  except Exception as e:
@@ -1122,6 +1138,7 @@ def curses_print(stdscr, hosts, threads, min_char_len = DEFAULT_CURSES_MINIMUM_C
1122
1138
  # We create all the windows we need
1123
1139
  # We initialize the color pair
1124
1140
  curses.start_color()
1141
+ curses.curs_set(0)
1125
1142
  curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
1126
1143
  curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_WHITE)
1127
1144
  curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
@@ -1204,7 +1221,7 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
1204
1221
  rtnStr = ''
1205
1222
  for output, hosts in outputs.items():
1206
1223
  if __global_suppress_printout:
1207
- rtnStr += f'Error returncode produced by {hosts}:\n'
1224
+ rtnStr += f'Abnormal returncode produced by {hosts}:\n'
1208
1225
  rtnStr += output+'\n'
1209
1226
  else:
1210
1227
  rtnStr += '*'*80+'\n'
@@ -1226,34 +1243,34 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
1226
1243
  print(rtnStr)
1227
1244
  return rtnStr
1228
1245
 
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
1246
+ # sshConfigged = False
1247
+ # def verify_ssh_config():
1248
+ # '''
1249
+ # Verify that ~/.ssh/config exists and contains the line "StrictHostKeyChecking no"
1250
+
1251
+ # Args:
1252
+ # None
1253
+
1254
+ # Returns:
1255
+ # None
1256
+ # '''
1257
+ # global sshConfigged
1258
+ # if not sshConfigged:
1259
+ # # first we make sure ~/.ssh/config exists
1260
+ # config = ''
1261
+ # if not os.path.exists(os.path.expanduser('~/.ssh')):
1262
+ # os.makedirs(os.path.expanduser('~/.ssh'))
1263
+ # if os.path.exists(os.path.expanduser('~/.ssh/config')):
1264
+ # with open(os.path.expanduser('~/.ssh/config'),'r') as f:
1265
+ # config = f.read()
1266
+ # if config:
1267
+ # if 'StrictHostKeyChecking no' not in config:
1268
+ # with open(os.path.expanduser('~/.ssh/config'),'a') as f:
1269
+ # f.write('\nHost *\n\tStrictHostKeyChecking no\n')
1270
+ # else:
1271
+ # with open(os.path.expanduser('~/.ssh/config'),'w') as f:
1272
+ # f.write('Host *\n\tStrictHostKeyChecking no\n')
1273
+ # sshConfigged = True
1257
1274
 
1258
1275
  def signal_handler(sig, frame):
1259
1276
  '''
@@ -1277,9 +1294,9 @@ def signal_handler(sig, frame):
1277
1294
  os.system(f'pkill -ef {os.path.basename(__file__)}')
1278
1295
  sys.exit(0)
1279
1296
 
1280
-
1281
1297
  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
1298
+ global __globalUnavailableHosts
1299
+ global _no_env
1283
1300
  threads = start_run_on_hosts(hosts, timeout=timeout,password=password,max_connections=max_connections)
1284
1301
  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
1302
  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 +1309,11 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
1292
1309
  # update the unavailable hosts and global unavailable hosts
1293
1310
  if willUpdateUnreachableHosts:
1294
1311
  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)
1312
+ __globalUnavailableHosts.update(unavailableHosts)
1313
+ # update the os environment variable if not _no_env
1314
+ if not _no_env:
1315
+ os.environ['__multiSSH3_UNAVAILABLE_HOSTS'] = ','.join(unavailableHosts)
1316
+
1296
1317
  # print the output, if the output of multiple hosts are the same, we aggragate them
1297
1318
  if not called:
1298
1319
  print_output(hosts,json,greppable=greppable)
@@ -1357,7 +1378,8 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
1357
1378
  scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1358
1379
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
1359
1380
  skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
1360
- single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY, shortend = False):
1381
+ single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,
1382
+ shortend = False):
1361
1383
  hosts = hosts if type(hosts) == str else frozenset(hosts)
1362
1384
  hostStr = formHostStr(hosts)
1363
1385
  files = frozenset(files) if files else None
@@ -1375,7 +1397,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1375
1397
  scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1376
1398
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
1377
1399
  skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
1378
- single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY):
1400
+ single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY,quiet = False):
1379
1401
  f'''
1380
1402
  Run the command on the hosts, aka multissh. main function
1381
1403
 
@@ -1394,6 +1416,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1394
1416
  interface_ip_prefix (str, optional): The prefix of the IPMI interface. Defaults to {DEFAULT_INTERFACE_IP_PREFIX}.
1395
1417
  returnUnfinished (bool, optional): Whether to return the unfinished hosts. Defaults to {_DEFAULT_RETURN_UNFINISHED}.
1396
1418
  scp (bool, optional): Whether to use scp instead of rsync. Defaults to {DEFAULT_SCP}.
1419
+ gather_mode (bool, optional): Whether to use gather mode. Defaults to False.
1397
1420
  username (str, optional): The username to use to connect to the hosts. Defaults to {DEFAULT_USERNAME}.
1398
1421
  extraargs (str, optional): Extra arguments to pass to the ssh / rsync / scp command. Defaults to {DEFAULT_EXTRA_ARGS}.
1399
1422
  skipUnreachable (bool, optional): Whether to skip unreachable hosts. Defaults to {DEFAULT_SKIP_UNREACHABLE}.
@@ -1406,12 +1429,22 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1406
1429
  min_line_len (int, optional): The minimum line number for each window of the curses output. Defaults to {DEFAULT_CURSES_MINIMUM_LINE_LEN}.
1407
1430
  single_window (bool, optional): Whether to use a single window for the curses output. Defaults to {DEFAULT_SINGLE_WINDOW}.
1408
1431
  file_sync (bool, optional): Whether to use file sync mode to sync directories. Defaults to {DEFAULT_FILE_SYNC}.
1432
+ error_only (bool, optional): Whether to only print the error output. Defaults to {DEFAULT_ERROR_ONLY}.
1433
+ quiet (bool, optional): Whether to suppress all verbose printout, added for compatibility, avoid using. Defaults to False.
1409
1434
 
1410
1435
  Returns:
1411
1436
  list: A list of Host objects
1412
1437
  '''
1413
- global __gloablUnavailableHosts
1438
+ global __globalUnavailableHosts
1414
1439
  global __global_suppress_printout
1440
+ global _no_env
1441
+ global _emo
1442
+ _emo = False
1443
+ _no_env = no_env
1444
+ if not no_env and '__multiSSH3_UNAVAILABLE_HOSTS' in os.environ:
1445
+ __globalUnavailableHosts = set(os.environ['__multiSSH3_UNAVAILABLE_HOSTS'].split(','))
1446
+ elif '__multiSSH3_UNAVAILABLE_HOSTS' in readEnvFromFile():
1447
+ __globalUnavailableHosts = set(readEnvFromFile()['__multiSSH3_UNAVAILABLE_HOSTS'].split(','))
1415
1448
  if not max_connections:
1416
1449
  max_connections = 4 * os.cpu_count()
1417
1450
  elif max_connections == 0:
@@ -1420,7 +1453,15 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1420
1453
  max_connections = (-max_connections) * os.cpu_count()
1421
1454
  if not commands:
1422
1455
  commands = []
1423
- verify_ssh_config()
1456
+ else:
1457
+ commands = [commands] if type(commands) == str else commands
1458
+ # reformat commands into a list of strings, join the iterables if they are not strings
1459
+ try:
1460
+ commands = [' '.join(command) if not type(command) == str else command for command in commands]
1461
+ except:
1462
+ pass
1463
+ print(f"Warning: commands should ideally be a list of strings. Now mssh had failed to convert {commands} to a list of strings. Continuing anyway but expect failures.")
1464
+ #verify_ssh_config()
1424
1465
  # load global unavailable hosts only if the function is called (so using --repeat will not load the unavailable hosts again)
1425
1466
  if called:
1426
1467
  # if called,
@@ -1429,18 +1470,18 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1429
1470
  if skipUnreachable is None:
1430
1471
  skipUnreachable = True
1431
1472
  if skipUnreachable:
1432
- unavailableHosts = __gloablUnavailableHosts
1473
+ unavailableHosts = __globalUnavailableHosts
1433
1474
  else:
1434
1475
  unavailableHosts = set()
1435
1476
  else:
1436
1477
  # if run in command line ( or emulating running in command line, we default to skip unreachable hosts within one command call )
1437
1478
  if skipUnreachable:
1438
- unavailableHosts = __gloablUnavailableHosts
1479
+ unavailableHosts = __globalUnavailableHosts
1439
1480
  else:
1440
1481
  unavailableHosts = set()
1441
1482
  skipUnreachable = True
1442
- global _emo
1443
- _emo = False
1483
+ if quiet:
1484
+ __global_suppress_printout = True
1444
1485
  # We create the hosts
1445
1486
  hostStr = formHostStr(hosts)
1446
1487
  skipHostStr = formHostStr(skip_hosts) if skip_hosts else ''
@@ -1459,8 +1500,8 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1459
1500
  if '@' not in host:
1460
1501
  skipHostStr[i] = userStr + host
1461
1502
  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)
1503
+ targetHostsList = expand_hostnames(frozenset(hostStr.split(',')))
1504
+ skipHostsList = expand_hostnames(frozenset(skipHostStr.split(',')))
1464
1505
  if skipHostsList:
1465
1506
  if not __global_suppress_printout: print(f"Skipping hosts: {skipHostsList}")
1466
1507
  if files and not commands:
@@ -1471,15 +1512,18 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
1471
1512
  files = set(files+commands) if files else set(commands)
1472
1513
  if files:
1473
1514
  # 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)
1515
+ if not gather_mode:
1516
+ pathSet = set()
1517
+ for file in files:
1518
+ try:
1519
+ pathSet.update(glob.glob(file,include_hidden=True,recursive=True))
1520
+ except:
1521
+ pathSet.update(glob.glob(file,recursive=True))
1522
+ if not pathSet:
1523
+ print(f'Warning: No source files at {files} are found after resolving globs!')
1524
+ sys.exit(66)
1525
+ else:
1526
+ pathSet = set(files)
1483
1527
  if file_sync:
1484
1528
  # use abosolute path for file sync
1485
1529
  commands = [os.path.abspath(file) for file in pathSet]
@@ -1598,6 +1642,7 @@ def get_default_config(args):
1598
1642
  'DEFAULT_GREPPABLE_MODE': args.greppable,
1599
1643
  'DEFAULT_SKIP_UNREACHABLE': args.skip_unreachable,
1600
1644
  'DEFAULT_SKIP_HOSTS': args.skip_hosts,
1645
+ 'SSH_STRICT_HOST_KEY_CHECKING': SSH_STRICT_HOST_KEY_CHECKING,
1601
1646
  'ERROR_MESSAGES_TO_IGNORE': ERROR_MESSAGES_TO_IGNORE,
1602
1647
  }
1603
1648
 
@@ -1605,14 +1650,15 @@ def write_default_config(args,CONFIG_FILE,backup = True):
1605
1650
  if backup and os.path.exists(CONFIG_FILE):
1606
1651
  os.rename(CONFIG_FILE,CONFIG_FILE+'.bak')
1607
1652
  default_config = get_default_config(args)
1653
+ # apply the updated defualt_config to __configs_from_file and write that to file
1654
+ __configs_from_file.update(default_config)
1608
1655
  with open(CONFIG_FILE,'w') as f:
1609
- json.dump(default_config,f,indent=4)
1656
+ json.dump(__configs_from_file,f,indent=4)
1610
1657
 
1611
1658
 
1612
1659
  def main():
1613
1660
  global _emo
1614
1661
  global __global_suppress_printout
1615
- global __gloablUnavailableHosts
1616
1662
  global __mainReturnCode
1617
1663
  global __failedHosts
1618
1664
  global __ipmiiInterfaceIPPrefix
@@ -1624,13 +1670,13 @@ def main():
1624
1670
  # We parse the arguments
1625
1671
  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}')
1626
1672
  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)
1627
- 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.')
1673
+ 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.')
1628
1674
  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)
1629
1675
  parser.add_argument('-p', '--password', type=str,help=f'The password to use to connect to the hosts, (default: {DEFAULT_PASSWORD})',default=DEFAULT_PASSWORD)
1630
1676
  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)
1631
1677
  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)
1632
1678
  parser.add_argument("-f","--file", action='append', help="The file to be copied to the hosts. Use -f multiple times to copy multiple files")
1633
- parser.add_argument('--file_sync', action='store_true', help=f'Operate in file sync mode, sync path in <COMMANDS> from this machine to <HOSTS>. Treat --file <FILE> and <COMMANDS> both as source as source and destination will be the same in this mode. (default: {DEFAULT_FILE_SYNC})', default=DEFAULT_FILE_SYNC)
1679
+ parser.add_argument('-fs','--file_sync', action='store_true', help=f'Operate in file sync mode, sync path in <COMMANDS> from this machine to <HOSTS>. Treat --file <FILE> and <COMMANDS> both as source as source and destination will be the same in this mode. (default: {DEFAULT_FILE_SYNC})', default=DEFAULT_FILE_SYNC)
1634
1680
  parser.add_argument('--scp', action='store_true', help=f'Use scp for copying files instead of rsync. Need to use this on windows. (default: {DEFAULT_SCP})', default=DEFAULT_SCP)
1635
1681
  parser.add_argument('-gm','--gather_mode', action='store_true', help=f'Gather files from the hosts instead of sending files to the hosts. Will send remote files specified in <FILE> to local path specified in <COMMANDS> (default: False)', default=False)
1636
1682
  #parser.add_argument("-d",'-c',"--destination", type=str, help="The destination of the files. Same as specify with commands. Added for compatibility. Use #HOST# or #HOSTNAME# to replace the host name in the destination")
@@ -1646,22 +1692,22 @@ def main():
1646
1692
  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
1693
  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
1694
  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)
1695
+ 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)
1696
+ 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
1697
  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
1698
  parser.add_argument("-j","--json", action='store_true', help=F"Output in json format. (default: {DEFAULT_JSON_MODE})", default=DEFAULT_JSON_MODE)
1653
1699
  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
1700
  parser.add_argument("-g","--greppable", action='store_true', help=f"Output in greppable format. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
1655
1701
  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
1702
  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}')
1703
+ 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
1704
  parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} with [ {", ".join(_binPaths.keys())} ] by {AUTHOR} ({AUTHOR_EMAIL})')
1659
1705
 
1660
1706
  # parser.add_argument('-u', '--user', metavar='user', type=str, nargs=1,
1661
1707
  # help='the user to use to connect to the hosts')
1662
1708
  args = parser.parse_args()
1663
1709
 
1664
- if args.generate_default_config_file:
1710
+ if args.store_config_file:
1665
1711
  try:
1666
1712
  if os.path.exists(CONFIG_FILE):
1667
1713
  print(f"Warning: {CONFIG_FILE} already exists, what to do? (o/b/n)")
@@ -1681,6 +1727,8 @@ def main():
1681
1727
  except Exception as e:
1682
1728
  print(f"Error while writing config file: {e}")
1683
1729
  if not args.commands:
1730
+ with open(CONFIG_FILE,'r') as f:
1731
+ print(f"Config file content: \n{f.read()}")
1684
1732
  sys.exit(0)
1685
1733
 
1686
1734
  _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,,