multiSSH3 6.17__tar.gz → 6.20__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 6.17
3
+ Version: 6.20
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 6.17
3
+ Version: 6.20
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
@@ -30,24 +30,130 @@ import threading
30
30
  import time
31
31
  import typing
32
32
  import uuid
33
- from collections import Counter, defaultdict, deque
33
+ from collections import Counter, defaultdict, deque, ChainMap
34
34
  from itertools import count, product
35
35
  from pprint import pformat
36
36
 
37
- __curses_available = False
38
- __resource_lib_available = False
39
- try:
40
- import curses
41
- import curses.panel
42
- __curses_available = True
43
- except ImportError:
44
- pass
45
- try:
46
- import resource
47
- __resource_lib_available = True
48
- except ImportError:
49
- pass
50
37
 
38
+ version = '6.20'
39
+ VERSION = version
40
+ __version__ = version
41
+ COMMIT_DATE = '2026-04-16'
42
+ # TODO: Add terminal TUI
43
+
44
+ CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
45
+ '~/multiSSH3.config.json',
46
+ '~/.multiSSH3.config.json',
47
+ '~/.config/multiSSH3/multiSSH3.config.json',
48
+ '/etc/multiSSH3.d/multiSSH3.config.json',
49
+ '/etc/multiSSH3.config.json'] # The first one has the highest priority
50
+
51
+ ERRORS = []
52
+
53
+ #%% ------------ User Defined Global Variables ----------------
54
+ AUTHOR = 'Yufei Pan'
55
+ AUTHOR_EMAIL = 'pan@zopyr.us'
56
+ DEFAULT_HOSTS = 'all'
57
+ DEFAULT_USERNAME = None
58
+ DEFAULT_PASSWORD = ''
59
+ DEFAULT_IDENTITY_FILE = None
60
+ DEFAULT_SSH_KEY_SEARCH_PATH = '~/.ssh/'
61
+ DEFAULT_USE_KEY = False
62
+ DEFAULT_EXTRA_ARGS = None
63
+ DEFAULT_ONE_ON_ONE = False
64
+ DEFAULT_SCP = False
65
+ DEFAULT_FILE_SYNC = False
66
+ DEFAULT_TIMEOUT = 50
67
+ DEFAULT_CLI_TIMEOUT = 0
68
+ DEFAULT_UNAVAILABLE_HOST_EXPIRY = 600
69
+ DEFAULT_REPEAT = 1
70
+ DEFAULT_INTERVAL = 0
71
+ DEFAULT_IPMI = False
72
+ DEFAULT_IPMI_INTERFACE_IP_PREFIX = ''
73
+ DEFAULT_IPMI_USERNAME = 'admin'
74
+ DEFAULT_IPMI_PASSWORD = 'admin'
75
+ DEFAULT_IPMI_ARGS=''
76
+ DEFAULT_IPMI_METHOD = 'ipmitool'
77
+ DEFAULT_IPMI_RETRY_LOCKOUT_TIME = -1
78
+ def get_default_ipmi_definition():
79
+ return {'interface_ip_prefix': DEFAULT_IPMI_INTERFACE_IP_PREFIX,
80
+ 'username': DEFAULT_IPMI_USERNAME,
81
+ 'password': DEFAULT_IPMI_PASSWORD,
82
+ 'args': DEFAULT_IPMI_ARGS,
83
+ 'ipmi_method': DEFAULT_IPMI_METHOD,
84
+ 'hostname_keyword': '',
85
+ 'retry_lockout_time': DEFAULT_IPMI_RETRY_LOCKOUT_TIME,
86
+ }
87
+ DEFAULT_IPMI_DEFINITIONS = [get_default_ipmi_definition()]
88
+ DEFAULT_INTERFACE_IP_PREFIX = None
89
+ DEFAULT_NO_WATCH = False
90
+ DEFAULT_WINDOW_WIDTH = 40
91
+ DEFAULT_WINDOW_HEIGHT = 1
92
+ DEFAULT_SINGLE_WINDOW = False
93
+ DEFAULT_ERROR_ONLY = False
94
+ DEFAULT_NO_OUTPUT = False
95
+ DEFAULT_RETURN_ZERO = False
96
+ DEFAULT_NO_ENV = False
97
+ DEFAULT_ENV_FILE = ''
98
+ DEFAULT_ENV_FILES = ['/etc/profile.d/hosts.sh',
99
+ '~/.bashrc',
100
+ '~/.zshrc',
101
+ '~/host.env',
102
+ '~/hosts.env',
103
+ '.env',
104
+ 'host.env',
105
+ 'hosts.env',
106
+ ]
107
+ DEFAULT_NO_HISTORY = False
108
+ DEFAULT_HISTORY_FILE = '~/.mssh_history'
109
+ DEFAULT_MAX_CONNECTIONS = 4 * os.cpu_count()
110
+ DEFAULT_JSON_MODE = False
111
+ DEFAULT_PRINT_SUCCESS_HOSTS = False
112
+ DEFAULT_GREPPABLE_MODE = False
113
+ DEFAULT_SKIP_UNREACHABLE = True
114
+ DEFAULT_SKIP_HOSTS = ''
115
+ DEFAULT_HOST_FILE = '/etc/hosts'
116
+ DEFAULT_ENCODING = 'utf-8'
117
+ DEFAULT_DIFF_DISPLAY_THRESHOLD = 0.9
118
+ SSH_STRICT_HOST_KEY_CHECKING = False
119
+ FORCE_TRUECOLOR = False
120
+ ERROR_MESSAGES_TO_IGNORE = [
121
+ 'Pseudo-terminal will not be allocated because stdin is not a terminal',
122
+ 'Connection to .* closed',
123
+ 'Warning: Permanently added',
124
+ 'mux_client_request_session',
125
+ 'disabling multiplexing',
126
+ 'Killed by signal',
127
+ 'Connection reset by peer',
128
+ ]
129
+ POSSIBLE_SSH_KEY_FILES = ['id_ed25519','id_ed25519_sk','id_ecdsa','id_ecdsa_sk','id_rsa','id_dsa']
130
+ __DEFAULT_COLOR_PALETTE = {
131
+ 'cyan': (86, 173, 188),
132
+ 'green': (114, 180, 43),
133
+ 'magenta': (140, 107, 200),
134
+ 'red': (196, 38, 94),
135
+ 'white': (227, 227, 221),
136
+ 'yellow': (179, 180, 43),
137
+ 'blue': (106, 126, 200),
138
+ 'bright_black': (102, 102, 102),
139
+ 'bright_blue': (129, 154, 255),
140
+ 'bright_cyan': (102, 217, 239),
141
+ 'bright_green': (126, 226, 46),
142
+ 'bright_magenta': (174, 129, 255),
143
+ 'bright_red': (249, 38, 114),
144
+ 'bright_white': (248, 248, 242),
145
+ 'bright_yellow': (226, 226, 46),
146
+ }
147
+ COLOR_PALETTE = __DEFAULT_COLOR_PALETTE.copy()
148
+ _DEFAULT_CALLED = True
149
+ _DEFAULT_RETURN_UNFINISHED = False
150
+ _DEFAULT_UPDATE_UNREACHABLE_HOSTS = True
151
+ _DEFAULT_NO_START = False
152
+ _etc_hosts = {}
153
+ __ERROR_MESSAGES_TO_IGNORE_REGEX =None
154
+ __DEBUG_MODE = False
155
+
156
+ #%% ------------ Pre Helper Functions ----------------
51
157
  try:
52
158
  # Check if functiools.cache is available
53
159
  # cache_decorator = functools.cache
@@ -85,23 +191,7 @@ except Exception:
85
191
  print('Warning: functools.lru_cache is not available, multiSSH3 will run slower without cache.',file=sys.stderr)
86
192
  def cache_decorator(func):
87
193
  return func
88
- version = '6.17'
89
- VERSION = version
90
- __version__ = version
91
- COMMIT_DATE = '2026-03-27'
92
-
93
- CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
94
- '~/multiSSH3.config.json',
95
- '~/.multiSSH3.config.json',
96
- '~/.config/multiSSH3/multiSSH3.config.json',
97
- '/etc/multiSSH3.d/multiSSH3.config.json',
98
- '/etc/multiSSH3.config.json'] # The first one has the highest priority
99
-
100
- ERRORS = []
101
-
102
- # TODO: Add terminal TUI
103
194
 
104
- #%% ------------ Pre Helper Functions ----------------
105
195
  def eprint(*args, **kwargs):
106
196
  global ERRORS
107
197
  try:
@@ -245,76 +335,6 @@ def _get_i():
245
335
  '''
246
336
  return next(_i_counter)
247
337
 
248
- #%% ------------ Host Object ----------------
249
- class Host:
250
- def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False,identity_file=None,shell=False,i = -1,uuid=uuid.uuid4(),ip = None):
251
- self.name = name # the name of the host (hostname or IP address)
252
- self.command = command # the command to run on the host
253
- self.returncode = None # the return code of the command
254
- self.output = [] # the output of the command for curses
255
- self.stdout = [] # the stdout of the command
256
- self.stderr = [] # the stderr of the command
257
- self.lineNumToPrintSet = set() # line numbers to reprint
258
- self.lastUpdateTime = time.monotonic() # the last time the output was updated
259
- self.lastPrintedUpdateTime = 0 # the last time the output was printed
260
- self.files = files # the files to be copied to the host
261
- self.ipmi = ipmi # whether to use ipmi to connect to the host
262
- self.shell = shell # whether to use shell to run the command
263
- self.interface_ip_prefix = interface_ip_prefix # the prefix of the ip address of the interface to be used to connect to the host
264
- self.scp = scp # whether to use scp to copy files to the host
265
- self.gatherMode = gatherMode # whether the host is in gather mode
266
- self.extraargs = extraargs # extra arguments to be passed to ssh
267
- self.resolvedName = None # the resolved IP address of the host
268
- # also store a globally unique integer i from 0
269
- self.i = i if i != -1 else _get_i()
270
- self.uuid = uuid
271
- self.identity_file = identity_file
272
- self.ip = ip if ip else getIP(name)
273
- self.current_color_pair = [-1, -1, 1]
274
- self.output_buffer = io.BytesIO()
275
- self.stdout_buffer = io.BytesIO()
276
- self.stderr_buffer = io.BytesIO()
277
- self.thread = None
278
-
279
- def __iter__(self):
280
- return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
281
- def __repr__(self):
282
- return f"Host(\n{pformat(self.__dict__)}\n)"
283
- def __str__(self):
284
- return f"Host(\n{pformat(dict(self))}\n)"
285
- def get_output_hash(self):
286
- return hash((
287
- self.command,
288
- tuple(self.stdout),
289
- tuple(self.stderr),
290
- self.returncode
291
- ))
292
- def copy(self):
293
- newHost = Host(
294
- name=self.name,
295
- command=self.command,
296
- files=self.files,
297
- ipmi=self.ipmi,
298
- interface_ip_prefix=self.interface_ip_prefix,
299
- scp=self.scp,
300
- extraargs=self.extraargs,
301
- gatherMode=self.gatherMode,
302
- identity_file=self.identity_file,
303
- shell=self.shell,
304
- i=self.i,
305
- uuid=self.uuid,
306
- ip=self.ip
307
- )
308
- newHost.returncode = self.returncode
309
- newHost.output = self.output.copy()
310
- newHost.stdout = self.stdout.copy()
311
- newHost.stderr = self.stderr.copy()
312
- newHost.lineNumToPrintSet = self.lineNumToPrintSet.copy()
313
- newHost.lastUpdateTime = self.lastUpdateTime
314
- newHost.lastPrintedUpdateTime = self.lastPrintedUpdateTime
315
- newHost.current_color_pair = self.current_color_pair.copy()
316
- return newHost
317
-
318
338
  #%% ------------ Load Defaults ( Config ) File ----------------
319
339
  def load_config_file(config_file):
320
340
  '''
@@ -336,96 +356,6 @@ def load_config_file(config_file):
336
356
  return {}
337
357
  return config
338
358
 
339
- #%% ------------ Global Variables ----------------
340
- AUTHOR = 'Yufei Pan'
341
- AUTHOR_EMAIL = 'pan@zopyr.us'
342
- DEFAULT_HOSTS = 'all'
343
- DEFAULT_USERNAME = None
344
- DEFAULT_PASSWORD = ''
345
- DEFAULT_IDENTITY_FILE = None
346
- DEFAULT_SSH_KEY_SEARCH_PATH = '~/.ssh/'
347
- DEFAULT_USE_KEY = False
348
- DEFAULT_EXTRA_ARGS = None
349
- DEFAULT_ONE_ON_ONE = False
350
- DEFAULT_SCP = False
351
- DEFAULT_FILE_SYNC = False
352
- DEFAULT_TIMEOUT = 50
353
- DEFAULT_CLI_TIMEOUT = 0
354
- DEFAULT_UNAVAILABLE_HOST_EXPIRY = 600
355
- DEFAULT_REPEAT = 1
356
- DEFAULT_INTERVAL = 0
357
- DEFAULT_IPMI = False
358
- DEFAULT_IPMI_INTERFACE_IP_PREFIX = ''
359
- DEFAULT_INTERFACE_IP_PREFIX = None
360
- DEFAULT_IPMI_USERNAME = 'ADMIN'
361
- DEFAULT_IPMI_PASSWORD = ''
362
- DEAFULT_IPMI_ARGS=''
363
- DEFAULT_NO_WATCH = False
364
- DEFAULT_WINDOW_WIDTH = 40
365
- DEFAULT_WINDOW_HEIGHT = 1
366
- DEFAULT_SINGLE_WINDOW = False
367
- DEFAULT_ERROR_ONLY = False
368
- DEFAULT_NO_OUTPUT = False
369
- DEFAULT_RETURN_ZERO = False
370
- DEFAULT_NO_ENV = False
371
- DEFAULT_ENV_FILE = ''
372
- DEFAULT_ENV_FILES = ['/etc/profile.d/hosts.sh',
373
- '~/.bashrc',
374
- '~/.zshrc',
375
- '~/host.env',
376
- '~/hosts.env',
377
- '.env',
378
- 'host.env',
379
- 'hosts.env',
380
- ]
381
- DEFAULT_NO_HISTORY = False
382
- DEFAULT_HISTORY_FILE = '~/.mssh_history'
383
- DEFAULT_MAX_CONNECTIONS = 4 * os.cpu_count()
384
- DEFAULT_JSON_MODE = False
385
- DEFAULT_PRINT_SUCCESS_HOSTS = False
386
- DEFAULT_GREPPABLE_MODE = False
387
- DEFAULT_SKIP_UNREACHABLE = True
388
- DEFAULT_SKIP_HOSTS = ''
389
- DEFAULT_HOST_FILE = '/etc/hosts'
390
- DEFAULT_ENCODING = 'utf-8'
391
- DEFAULT_DIFF_DISPLAY_THRESHOLD = 0.9
392
- SSH_STRICT_HOST_KEY_CHECKING = False
393
- FORCE_TRUECOLOR = False
394
- ERROR_MESSAGES_TO_IGNORE = [
395
- 'Pseudo-terminal will not be allocated because stdin is not a terminal',
396
- 'Connection to .* closed',
397
- 'Warning: Permanently added',
398
- 'mux_client_request_session',
399
- 'disabling multiplexing',
400
- 'Killed by signal',
401
- 'Connection reset by peer',
402
- ]
403
- __DEFAULT_COLOR_PALETTE = {
404
- 'cyan': (86, 173, 188),
405
- 'green': (114, 180, 43),
406
- 'magenta': (140, 107, 200),
407
- 'red': (196, 38, 94),
408
- 'white': (227, 227, 221),
409
- 'yellow': (179, 180, 43),
410
- 'blue': (106, 126, 200),
411
- 'bright_black': (102, 102, 102),
412
- 'bright_blue': (129, 154, 255),
413
- 'bright_cyan': (102, 217, 239),
414
- 'bright_green': (126, 226, 46),
415
- 'bright_magenta': (174, 129, 255),
416
- 'bright_red': (249, 38, 114),
417
- 'bright_white': (248, 248, 242),
418
- 'bright_yellow': (226, 226, 46),
419
- }
420
- COLOR_PALETTE = __DEFAULT_COLOR_PALETTE.copy()
421
- _DEFAULT_CALLED = True
422
- _DEFAULT_RETURN_UNFINISHED = False
423
- _DEFAULT_UPDATE_UNREACHABLE_HOSTS = True
424
- _DEFAULT_NO_START = False
425
- _etc_hosts = {}
426
- __ERROR_MESSAGES_TO_IGNORE_REGEX =None
427
- __DEBUG_MODE = False
428
-
429
359
  #%% Load Config Based Default Global variables
430
360
  __configs_from_file = {}
431
361
  for config_file in reversed(CONFIG_FILE_CHAIN.copy()):
@@ -449,8 +379,6 @@ __wildCharacters = ['*','?','x']
449
379
  _no_env = DEFAULT_NO_ENV
450
380
  _env_files = DEFAULT_ENV_FILES
451
381
  __globalUnavailableHosts = dict()
452
- __ipmiiInterfaceIPPrefix = DEFAULT_IPMI_INTERFACE_IP_PREFIX
453
- __ipmi_args = DEAFULT_IPMI_ARGS
454
382
  __keyPressesIn = [[]]
455
383
  _emo = False
456
384
  __curses_global_color_pairs = {(-1,-1):1}
@@ -467,12 +395,42 @@ __control_master_string = '''Host *
467
395
  ControlPath /run/user/%i/ssh_sockets_%C
468
396
  ControlPersist 3600
469
397
  '''
398
+ __curses_available = False
399
+ __resource_lib_available = False
400
+ try:
401
+ import curses
402
+ import curses.panel
403
+ __curses_available = True
404
+ except ImportError:
405
+ pass
406
+ try:
407
+ import resource
408
+ __resource_lib_available = True
409
+ except ImportError:
410
+ pass
470
411
  if __resource_lib_available:
471
412
  # Get the current limits
472
413
  _, __system_nofile_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
473
414
  # Set the soft limit to the hard limit
474
415
  resource.setrlimit(resource.RLIMIT_NOFILE, (__system_nofile_limit, __system_nofile_limit))
475
416
  __max_connections_nofile_limit_supported = int((__system_nofile_limit - 10) / 3)
417
+ _binPaths = {}
418
+ _binCalled = set(['sshpass', 'ssh', 'scp', 'ipmitool','rsync','sh','ssh-copy-id','redfishtool'])
419
+ def check_path(program_name):
420
+ global __configs_from_file
421
+ global _binPaths
422
+ config_key = f'_{program_name}Path'
423
+ program_path = (
424
+ __configs_from_file.get(config_key) or
425
+ globals().get(config_key) or
426
+ shutil.which(program_name)
427
+ )
428
+ if program_path:
429
+ _binPaths[program_name] = program_path
430
+ return True
431
+ return False
432
+
433
+ [check_path(program) for program in _binCalled]
476
434
 
477
435
  #%% Mapping of ANSI 4-bit colors to curses colors
478
436
  if __curses_available:
@@ -494,26 +452,78 @@ if __curses_available:
494
452
  96: curses.COLOR_CYAN, # Bright Cyan
495
453
  97: curses.COLOR_WHITE # Bright White
496
454
  }
497
- #%% ------------ Exportable Help Functions ----------------
498
- # check if command sshpass is available
499
- _binPaths = {}
500
- _binCalled = set(['sshpass', 'ssh', 'scp', 'ipmitool','rsync','sh','ssh-copy-id'])
501
- def check_path(program_name):
502
- global __configs_from_file
503
- global _binPaths
504
- config_key = f'_{program_name}Path'
505
- program_path = (
506
- __configs_from_file.get(config_key) or
507
- globals().get(config_key) or
508
- shutil.which(program_name)
509
- )
510
- if program_path:
511
- _binPaths[program_name] = program_path
512
- return True
513
- return False
514
455
 
515
- [check_path(program) for program in _binCalled]
456
+ #%% ------------ Host Object ----------------
457
+ class Host:
458
+ def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False,identity_file=None,shell=False,i = -1,uuid=uuid.uuid4(),ip = None):
459
+ self.name = name # the name of the host (hostname or IP address)
460
+ self.command = command # the command to run on the host
461
+ self.returncode = None # the return code of the command
462
+ self.output = [] # the output of the command for curses
463
+ self.stdout = [] # the stdout of the command
464
+ self.stderr = [] # the stderr of the command
465
+ self.lineNumToPrintSet = set() # line numbers to reprint
466
+ self.lastUpdateTime = time.monotonic() # the last time the output was updated
467
+ self.lastPrintedUpdateTime = 0 # the last time the output was printed
468
+ self.files = files # the files to be copied to the host
469
+ self.ipmi = ipmi # whether to use ipmi to connect to the host
470
+ self.shell = shell # whether to use shell to run the command
471
+ self.interface_ip_prefix = interface_ip_prefix # the prefix of the ip address of the interface to be used to connect to the host
472
+ self.scp = scp # whether to use scp to copy files to the host
473
+ self.gatherMode = gatherMode # whether the host is in gather mode
474
+ self.extraargs = extraargs # extra arguments to be passed to ssh
475
+ self.resolvedName = None # the resolved IP address of the host
476
+ # also store a globally unique integer i from 0
477
+ self.i = i if i != -1 else _get_i()
478
+ self.uuid = uuid
479
+ self.identity_file = identity_file
480
+ self.ip = ip if ip else getIP(name)
481
+ self.current_color_pair = [-1, -1, 1]
482
+ self.output_buffer = io.BytesIO()
483
+ self.stdout_buffer = io.BytesIO()
484
+ self.stderr_buffer = io.BytesIO()
485
+ self.thread = None
486
+
487
+ def __iter__(self):
488
+ return zip(['name', 'command', 'returncode', 'stdout', 'stderr'], [self.name, self.command, self.returncode, self.stdout, self.stderr])
489
+ def __repr__(self):
490
+ return f"Host(\n{pformat(self.__dict__)}\n)"
491
+ def __str__(self):
492
+ return f"Host(\n{pformat(dict(self))}\n)"
493
+ def get_output_hash(self):
494
+ return hash((
495
+ self.command,
496
+ tuple(self.stdout),
497
+ tuple(self.stderr),
498
+ self.returncode
499
+ ))
500
+ def copy(self):
501
+ newHost = Host(
502
+ name=self.name,
503
+ command=self.command,
504
+ files=self.files,
505
+ ipmi=self.ipmi,
506
+ interface_ip_prefix=self.interface_ip_prefix,
507
+ scp=self.scp,
508
+ extraargs=self.extraargs,
509
+ gatherMode=self.gatherMode,
510
+ identity_file=self.identity_file,
511
+ shell=self.shell,
512
+ i=self.i,
513
+ uuid=self.uuid,
514
+ ip=self.ip
515
+ )
516
+ newHost.returncode = self.returncode
517
+ newHost.output = self.output.copy()
518
+ newHost.stdout = self.stdout.copy()
519
+ newHost.stderr = self.stderr.copy()
520
+ newHost.lineNumToPrintSet = self.lineNumToPrintSet.copy()
521
+ newHost.lastUpdateTime = self.lastUpdateTime
522
+ newHost.lastPrintedUpdateTime = self.lastPrintedUpdateTime
523
+ newHost.current_color_pair = self.current_color_pair.copy()
524
+ return newHost
516
525
 
526
+ #%% ------------ Exportable Help Functions ----------------
517
527
  def find_ssh_key_file(searchPath = DEFAULT_SSH_KEY_SEARCH_PATH):
518
528
  '''
519
529
  Find the ssh public key file
@@ -528,8 +538,7 @@ def find_ssh_key_file(searchPath = DEFAULT_SSH_KEY_SEARCH_PATH):
528
538
  sshKeyPath = searchPath
529
539
  else:
530
540
  sshKeyPath ='~/.ssh'
531
- possibleSshKeyFiles = ['id_ed25519','id_ed25519_sk','id_ecdsa','id_ecdsa_sk','id_rsa','id_dsa']
532
- for sshKeyFile in possibleSshKeyFiles:
541
+ for sshKeyFile in POSSIBLE_SSH_KEY_FILES:
533
542
  if os.path.exists(os.path.expanduser(os.path.join(sshKeyPath,sshKeyFile))):
534
543
  return os.path.join(sshKeyPath,sshKeyFile)
535
544
  return None
@@ -1790,7 +1799,7 @@ def __handle_writing_stream(stream,stop_event,host):
1790
1799
  # host.stdout.append(' $ ' + ''.join(__keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
1791
1800
  return sentInputPos
1792
1801
 
1793
- def run_command(host, sem, timeout=60,passwds=None, retry_limit = 7,ipmi_args = ...):
1802
+ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 7 + len(DEFAULT_IPMI_DEFINITIONS),ipmi_definitions_list = ...):
1794
1803
  '''
1795
1804
  Run the command on the host. Will format the commands accordingly. Main execution function.
1796
1805
 
@@ -1805,45 +1814,67 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 7,ipmi_args =
1805
1814
  '''
1806
1815
  global _emo
1807
1816
  global __ERROR_MESSAGES_TO_IGNORE_REGEX
1808
- global __ipmiiInterfaceIPPrefix
1809
1817
  global _binPaths
1810
1818
  global __DEBUG_MODE
1811
- global DEFAULT_IPMI_USERNAME
1812
- global DEFAULT_IPMI_PASSWORD
1813
1819
  global DEFAULT_USERNAME
1814
1820
  global DEFAULT_PASSWORD
1815
1821
  global SSH_STRICT_HOST_KEY_CHECKING
1816
- global __ipmi_args
1817
- global DEAFULT_IPMI_ARGS
1818
1822
  if retry_limit < 0:
1819
1823
  host.output.append('Error: Retry limit reached!')
1820
1824
  host.stderr.append('Error: Retry limit reached!')
1821
1825
  host.returncode = 1
1822
1826
  return
1823
- if ipmi_args is ...:
1824
- ipmi_args = __ipmi_args
1825
- prePasword = passwds
1827
+ ipmi_definition = {}
1828
+ username = None
1829
+ sshLocalExtraArgs = []
1830
+ ip_prefix = None
1826
1831
  try:
1827
- localExtraArgs = []
1828
-
1829
1832
  if not SSH_STRICT_HOST_KEY_CHECKING:
1830
- localExtraArgs = ['-o StrictHostKeyChecking=no','-o UserKnownHostsFile=/dev/null']
1833
+ sshLocalExtraArgs = ['-o StrictHostKeyChecking=no','-o UserKnownHostsFile=/dev/null']
1831
1834
  if host.identity_file:
1832
- localExtraArgs += ['-i',host.identity_file]
1833
- rsyncLocalExtraArgs = ['--rsh','ssh ' + ' '.join(localExtraArgs)]
1834
- host.username = None
1835
+ sshLocalExtraArgs += ['-i',host.identity_file]
1836
+ rsyncLocalExtraArgs = ['--rsh','ssh ' + ' '.join(sshLocalExtraArgs)]
1835
1837
  host.address = host.name
1836
1838
  if '@' in host.name:
1837
- host.username, host.address = host.name.rsplit('@',1)
1839
+ username, host.address = host.name.rsplit('@',1)
1840
+ if host.ipmi:
1841
+ if ipmi_definitions_list is ...:
1842
+ ipmi_definitions_list = [ipmi_definition for ipmi_definition in DEFAULT_IPMI_DEFINITIONS if not ipmi_definition.get('hostname_keyword') or str(ipmi_definition.get('hostname_keyword')).strip() in host.name]
1843
+ if not ipmi_definitions_list:
1844
+ host.output.append('Error: Exhausted all matching ipmi definitions!')
1845
+ host.stderr.append('Error: Exhausted all matching ipmi definitions!')
1846
+ host.returncode = 1
1847
+ return
1848
+ ipmi_definition = dict(ChainMap(ipmi_definitions_list.pop(0),get_default_ipmi_definition()))
1849
+ if ipmi_definition.get('ipmi_method') == 'ssh_ipmitool':
1850
+ if not host.command:
1851
+ host.command = 'ipmitool power status'
1852
+ else:
1853
+ host.command = 'ipmitool '+host.command if not host.command.startswith('ipmitool ') else host.command
1854
+ if username:
1855
+ host.output.append(f'Warning:-u SSH_USERNAME {username} is provided in the host name! Using the username from the ipmi definition if provided: {ipmi_definition.get("username")}')
1856
+ if __DEBUG_MODE:
1857
+ host.stderr.append(f'Warning:-u SSH_USERNAME {username} is provided in the host name! Using the username from the ipmi definition if provided: {ipmi_definition.get("username")}')
1858
+ username = ipmi_definition.get('username',username)
1859
+ if passwds:
1860
+ host.output.append(f'Warning: -p SSH_PASSWORD {passwds} is provided! Using the password from the ipmi definition if provided: {ipmi_definition.get("password")}')
1861
+ if __DEBUG_MODE:
1862
+ host.stderr.append(f'Warning: -p SSH_PASSWORD {passwds} is provided! Using the password from the ipmi definition if provided: {ipmi_definition.get("password")}')
1863
+ passwds = ipmi_definition.get('password',passwds)
1864
+ if ipmi_definition.get('interface_ip_prefix') and ipmi_definition.get('interface_ip_prefix') == DEFAULT_IPMI_INTERFACE_IP_PREFIX:
1865
+ host.output.append('Warning: ipmi_interface_ip_prefix that is the same as the DEFAULT_IPMI_INTERFACE_IP_PREFIX is ignored in ssh_ipmitool mode.')
1866
+ if __DEBUG_MODE:
1867
+ host.stderr.append('Warning: ipmi_interface_ip_prefix that is the same as the DEFAULT_IPMI_INTERFACE_IP_PREFIX is ignored in ssh_ipmitool mode.')
1868
+ else:
1869
+ ip_prefix = ipmi_definition.get('interface_ip_prefix')
1838
1870
  host.command = replace_magic_strings(host.command,['#HOST#','#HOSTNAME#'],host.address,case_sensitive=False)
1839
- if host.username:
1840
- host.command = replace_magic_strings(host.command,['#USER#','#USERNAME#'],host.username,case_sensitive=False)
1871
+ if username:
1872
+ host.command = replace_magic_strings(host.command,['#USER#','#USERNAME#'],username,case_sensitive=False)
1841
1873
  else:
1842
1874
  current_user = getpass.getuser()
1843
1875
  host.command = replace_magic_strings(host.command,['#USER#','#USERNAME#'],current_user,case_sensitive=False)
1844
1876
  host.command = replace_magic_strings(host.command,['#ID#'],str(id(host)),case_sensitive=False)
1845
1877
  host.command = replace_magic_strings(host.command,['#I#'],str(host.i),case_sensitive=False)
1846
- host.command = replace_magic_strings(host.command,['#PASSWD#','#PASSWORD#'],passwds,case_sensitive=False)
1847
1878
  host.command = replace_magic_strings(host.command,['#UUID#'],str(host.uuid),case_sensitive=False)
1848
1879
  host.command = replace_magic_strings(host.command,['#IP#'],str(host.ip),case_sensitive=False)
1849
1880
  formatedCMD = []
@@ -1853,63 +1884,65 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 7,ipmi_args =
1853
1884
  extraargs = [str(arg) for arg in host.extraargs]
1854
1885
  else:
1855
1886
  extraargs = []
1856
- if __ipmiiInterfaceIPPrefix:
1857
- host.interface_ip_prefix = __ipmiiInterfaceIPPrefix if host.ipmi and not host.interface_ip_prefix else host.interface_ip_prefix
1858
- if host.interface_ip_prefix:
1887
+ if not ip_prefix:
1888
+ ip_prefix = host.interface_ip_prefix
1889
+ if ip_prefix:
1859
1890
  try:
1860
1891
  hostOctets = host.ip.split('.')
1861
- prefixOctets = host.interface_ip_prefix.split('.')
1892
+ prefixOctets = ip_prefix.split('.')
1862
1893
  host.address = '.'.join(prefixOctets[:3]+hostOctets[min(3,len(prefixOctets)):])
1863
- host.resolvedName = host.username + '@' if host.username else ''
1864
- host.resolvedName += host.address
1865
1894
  except Exception:
1866
- host.resolvedName = host.name
1895
+ host.output.append(f'Warning: Failed to resolve the IP address for the host {host.name}. Unable to resolve ip_prefix {ip_prefix}!')
1896
+ if __DEBUG_MODE:
1897
+ host.stderr.append(f'Warning: Failed to resolve the IP address for the host {host.name}. Unable to resolve ip_prefix {ip_prefix}!')
1898
+ if username:
1899
+ host.resolvedName = username + '@' + host.address
1867
1900
  else:
1868
- host.resolvedName = host.name
1901
+ host.resolvedName = host.address
1869
1902
  if host.resolvedName:
1870
1903
  host.command = replace_magic_strings(host.command,['#RESOLVEDNAME#','#RESOLVED#'],host.resolvedName,case_sensitive=False)
1871
- if host.ipmi:
1872
- if 'ipmitool' in _binPaths:
1904
+ if __DEBUG_MODE:
1905
+ host.stderr.append(f'Resolved name: {host.resolvedName}')
1906
+ if host.ipmi and ipmi_definition.get('ipmi_method') != 'ssh_ipmitool':
1907
+ #{interface_ip_prefix,username,password,args,ipmi_method,hostname_keyword,retry_lockout_time}
1908
+ if ipmi_definition.get('ipmi_method') == 'ipmitool':
1909
+ if 'ipmitool' not in _binPaths:
1910
+ host.output.append('Ipmitool not found on the local machine! Trying ipmitool on the remote machine...')
1911
+ if __DEBUG_MODE:
1912
+ host.stderr.append('Ipmitool not found on the local machine! Trying ipmitool on the remote machine...')
1913
+ if ipmi_definition.get('interface_ip_prefix') == DEFAULT_IPMI_INTERFACE_IP_PREFIX:
1914
+ ipmi_definition['interface_ip_prefix'] = DEFAULT_INTERFACE_IP_PREFIX
1915
+ ipmi_definition['username'] = username
1916
+ ipmi_definition['password'] = passwds
1917
+ ipmi_definition['ipmi_method'] = 'ssh_ipmitool'
1918
+ ipmi_definitions_list.insert(0,ipmi_definition)
1919
+ return run_command(host,sem,timeout,passwds,retry_limit=retry_limit - 1,ipmi_definitions_list=ipmi_definitions_list)
1873
1920
  if host.command.startswith('ipmitool '):
1874
1921
  host.command = host.command.replace('ipmitool ','')
1875
1922
  elif host.command.startswith(_binPaths['ipmitool']):
1876
1923
  host.command = host.command.replace(_binPaths['ipmitool'],'')
1877
- if not host.username or host.username == DEFAULT_USERNAME:
1878
- if DEFAULT_IPMI_USERNAME:
1879
- host.username = DEFAULT_IPMI_USERNAME
1880
- elif DEFAULT_USERNAME:
1881
- host.username = DEFAULT_USERNAME
1882
- else:
1883
- host.username = 'ADMIN'
1884
- if not passwds or passwds == DEFAULT_PASSWORD:
1885
- if DEFAULT_IPMI_PASSWORD:
1886
- passwds = DEFAULT_IPMI_PASSWORD
1887
- elif DEFAULT_PASSWORD:
1888
- passwds = DEFAULT_PASSWORD
1889
- else:
1890
- host.output.append('Warning: Password not provided for ipmi! Using a default password `admin`.')
1891
- passwds = 'admin'
1892
1924
  if not host.command:
1893
1925
  host.command = 'power status'
1926
+ ipmi_args = ipmi_definition.get('args','')
1894
1927
  if 'sh' in _binPaths:
1895
- formatedCMD = [_binPaths['sh'],'-c',f'ipmitool -H {host.address} -U {host.username} -P {passwds} {ipmi_args} {" ".join(extraargs)} {host.command}']
1928
+ formatedCMD = [_binPaths['sh'],'-c',f'ipmitool -H {host.address} -U {ipmi_definition.get("username",username)} -P {ipmi_definition.get("password",passwds)} {ipmi_args} {host.command}']
1896
1929
  else:
1897
- formatedCMD = [_binPaths['ipmitool'],f'-H {host.address}',f'-U {host.username}',f'-P {passwds}'] + [f'-{s}' for s in ipmi_args.split('-') if s] + extraargs + [host.command]
1898
- elif 'ssh' in _binPaths:
1899
- host.output.append('Ipmitool not found on the local machine! Trying ipmitool on the remote machine...')
1900
- if __DEBUG_MODE:
1901
- host.stderr.append('Ipmitool not found on the local machine! Trying ipmitool on the remote machine...')
1902
- host.ipmi = False
1903
- host.interface_ip_prefix = None
1930
+ formatedCMD = [_binPaths['ipmitool'],f'-H {host.address}',f'-U {ipmi_definition.get("username",username)}',f'-P {ipmi_definition.get("password",passwds)}'] + [f'-{s}' for s in ipmi_args.split('-') if s] + [host.command]
1931
+ elif ipmi_definition.get('ipmi_method') == 'redfish':
1932
+ if host.command.startswith('redfishtool '):
1933
+ host.command = host.command.replace('redfishtool ','')
1934
+ elif host.command.startswith(_binPaths['redfishtool']):
1935
+ host.command = host.command.replace(_binPaths['redfishtool'],'')
1904
1936
  if not host.command:
1905
- host.command = 'ipmitool power status'
1937
+ host.command = 'Systems'
1938
+ ipmi_args = ipmi_definition.get('args','')
1939
+ if 'sh' in _binPaths:
1940
+ formatedCMD = [_binPaths['sh'],'-c',f'redfishtool -S Always -a -r {host.address} -u {ipmi_definition.get("username",username)} -p {ipmi_definition.get("password",passwds)} {ipmi_args} {host.command}']
1906
1941
  else:
1907
- host.command = 'ipmitool '+host.command if not host.command.startswith('ipmitool ') else host.command
1908
- run_command(host,sem,timeout,prePasword,retry_limit=retry_limit - 1,ipmi_args=ipmi_args)
1909
- return
1942
+ formatedCMD = [_binPaths['redfishtool'],'-S', 'Always','-a','-r',host.address,'-u',ipmi_definition.get("username",username),'-p',ipmi_definition.get("password",passwds)] + [f'-{s}' for s in ipmi_args.split('-') if s] + [host.command]
1910
1943
  else:
1911
- host.output.append('Ipmitool not found on the local machine! Please install ipmitool to use ipmi mode.')
1912
- host.stderr.append('Ipmitool not found on the local machine! Please install ipmitool to use ipmi mode.')
1944
+ host.output.append(f'IPMI method {ipmi_definition.get("ipmi_method","None")} not supported!')
1945
+ host.stderr.append(f'IPMI method {ipmi_definition.get("ipmi_method","None")} not supported!')
1913
1946
  host.returncode = 1
1914
1947
  return
1915
1948
  elif host.shell:
@@ -1924,7 +1957,7 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 7,ipmi_args =
1924
1957
  host.stderr.append('shell not found on the local machine! Using ssh localhost instead...')
1925
1958
  host.shell = False
1926
1959
  host.name = 'localhost'
1927
- run_command(host,sem,timeout,prePasword,retry_limit=retry_limit - 1,ipmi_args=ipmi_args)
1960
+ run_command(host,sem,timeout,passwds,retry_limit=retry_limit - 1,ipmi_definitions_list=ipmi_definitions_list)
1928
1961
  else:
1929
1962
  if host.files:
1930
1963
  if host.scp:
@@ -1957,11 +1990,11 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 7,ipmi_args =
1957
1990
  else:
1958
1991
  fileArgs = host.files + [f'{host.resolvedName}:{host.command}']
1959
1992
  if useScp:
1960
- formatedCMD = [_binPaths['scp'],'-rp'] + localExtraArgs + extraargs +['--']+fileArgs
1993
+ formatedCMD = [_binPaths['scp'],'-rp'] + sshLocalExtraArgs + extraargs +['--']+fileArgs
1961
1994
  else:
1962
1995
  formatedCMD = [_binPaths['rsync'],'-ahlX','--partial','--inplace', '--info=name'] + rsyncLocalExtraArgs + extraargs +['--']+fileArgs
1963
1996
  else:
1964
- formatedCMD = [_binPaths['ssh']] + localExtraArgs + extraargs +['--']+ [host.resolvedName, host.command]
1997
+ formatedCMD = [_binPaths['ssh']] + sshLocalExtraArgs + extraargs +['--']+ [host.resolvedName, host.command]
1965
1998
  if passwds and 'sshpass' in _binPaths:
1966
1999
  formatedCMD = [_binPaths['sshpass'], '-p', passwds] + formatedCMD
1967
2000
  elif passwds:
@@ -2078,7 +2111,7 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 7,ipmi_args =
2078
2111
  host.output.append("Warning: Too many open files. retrying...")
2079
2112
  # Handle the error, e.g., clean up, retry logic, or exit
2080
2113
  time.sleep(0.1)
2081
- run_command(host,sem,timeout,prePasword,retry_limit=retry_limit - 1,ipmi_args=ipmi_args)
2114
+ run_command(host,sem,timeout,passwds,retry_limit=retry_limit - 1,ipmi_definitions_list=ipmi_definitions_list)
2082
2115
  else:
2083
2116
  # Re-raise the exception if it's not the specific one
2084
2117
  raise
@@ -2089,39 +2122,47 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 7,ipmi_args =
2089
2122
  host.stderr.extend(traceback.format_exc().split('\n'))
2090
2123
  host.output.extend(traceback.format_exc().split('\n'))
2091
2124
  host.returncode = -1
2092
- # If using ipmi, we will try again using ssh if ipmi connection is not successful
2093
- if host.ipmi and host.returncode != 0 and any(['Unable to establish IPMI' in line for line in host.stderr]):
2125
+ # If using ipmi, we will try again using next ipmi definition if ipmi connection is not successful
2126
+ if host.ipmi and host.returncode != 0:
2094
2127
  host.returncode = None
2095
- host.stderr = []
2096
- if ipmi_args:
2097
- if DEAFULT_IPMI_ARGS and ipmi_args != DEAFULT_IPMI_ARGS:
2098
- host.output.append(f'IPMI connection failed with arg {ipmi_args}! Trying DEFAULT_IPMI_ARGS:{DEAFULT_IPMI_ARGS}')
2099
- if __DEBUG_MODE:
2100
- host.stderr.append(f'IPMI connection failed with arg {ipmi_args}! Trying DEFAULT_IPMI_ARGS:{DEAFULT_IPMI_ARGS}')
2101
- ipmi_args = DEAFULT_IPMI_ARGS
2102
- else:
2103
- host.output.append(f'IPMI connection failed with arg {ipmi_args}! Trying without args...')
2104
- if __DEBUG_MODE:
2105
- host.stderr.append(f'IPMI connection failed with arg {ipmi_args}! Trying without args...')
2106
- ipmi_args = ''
2128
+ host.output.append('IPMI connection failed! Trying next ipmi definition...')
2129
+ if __DEBUG_MODE:
2130
+ host.stderr.append('IPMI connection failed! Trying next ipmi definition...')
2107
2131
  else:
2108
- host.output.append('IPMI connection failed! Trying SSH connection...')
2109
- if __DEBUG_MODE:
2110
- host.stderr.append('IPMI connection failed! Trying SSH connection...')
2111
- host.ipmi = False
2112
- host.interface_ip_prefix = None
2113
- host.command = 'ipmitool '+host.command if not host.command.startswith('ipmitool ') else host.command
2114
- run_command(host,sem,timeout,prePasword,retry_limit=retry_limit - 1,ipmi_args=ipmi_args)
2132
+ host.stdout = []
2133
+ host.stderr = []
2134
+ if ipmi_definition.get('retry_lockout_time'):
2135
+ try:
2136
+ sleep_seconds = int(ipmi_definition.get('retry_lockout_time'))
2137
+ if sleep_seconds >= 0:
2138
+ host.output.append(f'Sleeping for {sleep_seconds} seconds...')
2139
+ if __DEBUG_MODE:
2140
+ host.stderr.append(f'Sleeping for {sleep_seconds} seconds...')
2141
+ time.sleep(sleep_seconds)
2142
+ except Exception as e:
2143
+ host.output.append(f'Error: Failed to sleep for {ipmi_definition.get("retry_lockout_time")} seconds! {e}')
2144
+ if __DEBUG_MODE:
2145
+ host.stderr.append(f'Error: Failed to sleep for {ipmi_definition.get("retry_lockout_time")} seconds! {e}')
2146
+ time.sleep(DEFAULT_IPMI_RETRY_LOCKOUT_TIME)
2147
+ if ipmi_definition.get('ipmi_method') == 'ipmitool':
2148
+ if ipmi_definition.get('interface_ip_prefix') == DEFAULT_IPMI_INTERFACE_IP_PREFIX:
2149
+ ipmi_definition['interface_ip_prefix'] = DEFAULT_INTERFACE_IP_PREFIX
2150
+ ipmi_definition['username'] = username
2151
+ ipmi_definition['password'] = passwds
2152
+ ipmi_definition['ipmi_method'] = 'ssh_ipmitool'
2153
+ ipmi_definitions_list.append(ipmi_definition)
2154
+ run_command(host,sem,timeout,passwds,retry_limit=retry_limit - 1,ipmi_definitions_list=ipmi_definitions_list)
2115
2155
  # If transfering files, we will try again using scp if rsync connection is not successful
2116
2156
  elif host.files and not host.scp and not useScp and host.returncode != 0 and host.stderr:
2117
2157
  host.returncode = None
2118
- host.stderr = []
2119
- host.stdout = []
2120
2158
  host.output.append('Rsync connection failed! Trying SCP connection...')
2121
2159
  if __DEBUG_MODE:
2122
2160
  host.stderr.append('Rsync connection failed! Trying SCP connection...')
2161
+ else:
2162
+ host.stdout = []
2163
+ host.stderr = []
2123
2164
  host.scp = True
2124
- run_command(host,sem,timeout,prePasword,retry_limit=retry_limit - 1,ipmi_args=ipmi_args)
2165
+ run_command(host,sem,timeout,passwds,retry_limit=retry_limit - 1,ipmi_definitions_list=ipmi_definitions_list)
2125
2166
 
2126
2167
  #%% ------------ Start Threading Block ----------------
2127
2168
  def start_run_on_hosts(hosts, timeout=60,password=None,max_connections=4 * os.cpu_count()):
@@ -3357,7 +3398,6 @@ def processRunOnHosts(timeout, password, max_connections, hosts, return_unfinish
3357
3398
 
3358
3399
 
3359
3400
  #%% ------------ Stringfy Block ----------------
3360
-
3361
3401
  def formHostStr(host) -> str:
3362
3402
  """
3363
3403
  Forms a comma-separated string of hosts.
@@ -3380,16 +3420,35 @@ def formHostStr(host) -> str:
3380
3420
  return ','.join(compact_hostnames(host))
3381
3421
 
3382
3422
  @cache_decorator
3383
- def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
3384
- no_watch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,max_connections=DEFAULT_MAX_CONNECTIONS,
3385
- file = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,
3386
- scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skip_unreachable=DEFAULT_SKIP_UNREACHABLE,
3387
- no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,skip_hosts = DEFAULT_SKIP_HOSTS,
3388
- file_sync = False, error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
3389
- copy_id = False, unavailable_host_expiry = DEFAULT_UNAVAILABLE_HOST_EXPIRY, no_history = DEFAULT_NO_HISTORY,
3390
- history_file = DEFAULT_HISTORY_FILE, env_file = '', env_files = DEFAULT_ENV_FILES,
3391
- repeat = DEFAULT_REPEAT,interval = DEFAULT_INTERVAL,
3392
- shortend = False) -> str:
3423
+ def __formCommandArgStr(shortend = False, **kwargs) -> str:
3424
+ oneonone = kwargs.get('oneonone', DEFAULT_ONE_ON_ONE)
3425
+ timeout = kwargs.get('timeout', DEFAULT_TIMEOUT)
3426
+ password = kwargs.get('password', DEFAULT_PASSWORD)
3427
+ no_watch = kwargs.get('no_watch', DEFAULT_NO_WATCH)
3428
+ json = kwargs.get('json', DEFAULT_JSON_MODE)
3429
+ max_connections = kwargs.get('max_connections', DEFAULT_MAX_CONNECTIONS)
3430
+ file = kwargs.get('file', None)
3431
+ ipmi = kwargs.get('ipmi', DEFAULT_IPMI)
3432
+ interface_ip_prefix = kwargs.get('interface_ip_prefix', DEFAULT_INTERFACE_IP_PREFIX)
3433
+ scp = kwargs.get('scp', DEFAULT_SCP)
3434
+ gather_mode = kwargs.get('gather_mode', False)
3435
+ username = kwargs.get('username', DEFAULT_USERNAME)
3436
+ extraargs = kwargs.get('extraargs', DEFAULT_EXTRA_ARGS)
3437
+ skip_unreachable = kwargs.get('skip_unreachable', DEFAULT_SKIP_UNREACHABLE)
3438
+ no_env = kwargs.get('no_env', DEFAULT_NO_ENV)
3439
+ greppable = kwargs.get('greppable', DEFAULT_GREPPABLE_MODE)
3440
+ skip_hosts = kwargs.get('skip_hosts', DEFAULT_SKIP_HOSTS)
3441
+ file_sync = kwargs.get('file_sync', False)
3442
+ error_only = kwargs.get('error_only', DEFAULT_ERROR_ONLY)
3443
+ identity_file = kwargs.get('identity_file', DEFAULT_IDENTITY_FILE)
3444
+ copy_id = kwargs.get('copy_id', False)
3445
+ unavailable_host_expiry = kwargs.get('unavailable_host_expiry', DEFAULT_UNAVAILABLE_HOST_EXPIRY)
3446
+ no_history = kwargs.get('no_history', DEFAULT_NO_HISTORY)
3447
+ history_file = kwargs.get('history_file', DEFAULT_HISTORY_FILE)
3448
+ env_file = kwargs.get('env_file', '')
3449
+ env_files = kwargs.get('env_files', DEFAULT_ENV_FILES)
3450
+ repeat = kwargs.get('repeat', DEFAULT_REPEAT)
3451
+ interval = kwargs.get('interval', DEFAULT_INTERVAL)
3393
3452
  argsList = []
3394
3453
  if oneonone:
3395
3454
  argsList.append('--oneonone' if not shortend else '-11')
@@ -3451,37 +3510,22 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
3451
3510
  argsList.append('--file_sync' if not shortend else '-fs')
3452
3511
  return ' '.join(argsList)
3453
3512
 
3454
- def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
3455
- no_watch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
3456
- file = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,return_unfinished = _DEFAULT_RETURN_UNFINISHED,
3457
- scp=DEFAULT_SCP,gather_mode = False,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skip_unreachable=DEFAULT_SKIP_UNREACHABLE,
3458
- no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,will_update_unreachable_hosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
3459
- skip_hosts = DEFAULT_SKIP_HOSTS, window_width = DEFAULT_WINDOW_WIDTH, window_height = DEFAULT_WINDOW_HEIGHT,
3460
- single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
3461
- copy_id = False, unavailable_host_expiry = DEFAULT_UNAVAILABLE_HOST_EXPIRY,no_history = DEFAULT_NO_HISTORY,
3462
- history_file = DEFAULT_HISTORY_FILE, env_file = '', env_files = [],
3463
- repeat = DEFAULT_REPEAT,interval = DEFAULT_INTERVAL,
3464
- shortend = False,tabSeperated = False,**kwargs) -> str:
3465
- _ = called
3466
- _ = return_unfinished
3467
- _ = will_update_unreachable_hosts
3468
- _ = no_start
3469
- _ = window_width
3470
- _ = window_height
3471
- _ = single_window
3513
+ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,shortend = False,tabSeperated = False,**kwargs) -> str:
3514
+ # Allow explicit kwargs to override base parameters when forwarding dicts.
3515
+ hosts = kwargs.get('hosts', hosts)
3516
+ commands = kwargs.get('commands', commands)
3517
+ shortend = kwargs.get('shortend', shortend)
3518
+ tabSeperated = kwargs.get('tabSeperated', tabSeperated)
3472
3519
  hosts = hosts if isinstance(hosts,str) else frozenset(hosts)
3473
3520
  hostStr = formHostStr(hosts)
3474
- file = frozenset(file) if file else None
3475
- argsStr = __formCommandArgStr(oneonone = oneonone, timeout = timeout,password = password,
3476
- no_watch = no_watch,json = json,max_connections=max_connections,
3477
- file = file,ipmi = ipmi,interface_ip_prefix = interface_ip_prefix,
3478
- scp=scp,gather_mode = gather_mode,username=username,extraargs=extraargs,skip_unreachable=skip_unreachable,
3479
- no_env=no_env, greppable=greppable,skip_hosts = skip_hosts,
3480
- file_sync = file_sync,error_only = error_only, identity_file = identity_file,
3481
- copy_id = copy_id, unavailable_host_expiry =unavailable_host_expiry,no_history = no_history,
3482
- history_file = history_file, env_file = env_file, env_files = env_files,
3483
- repeat = repeat,interval = interval,
3484
- shortend = shortend)
3521
+ commandArgKwargs = dict(kwargs)
3522
+ commandArgKwargs.pop('hosts', None)
3523
+ commandArgKwargs.pop('commands', None)
3524
+ commandArgKwargs.pop('shortend', None)
3525
+ commandArgKwargs.pop('tabSeperated', None)
3526
+ if commandArgKwargs.get('file'):
3527
+ commandArgKwargs['file'] = frozenset(commandArgKwargs['file'])
3528
+ argsStr = __formCommandArgStr(shortend = shortend, **commandArgKwargs)
3485
3529
  commands = [command.replace('"', '\\"').replace('\n', '\\n').replace('\t', '\\t') for command in format_commands(commands)]
3486
3530
  commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
3487
3531
  filePath = os.path.abspath(__file__)
@@ -3505,15 +3549,11 @@ def record_command_history(kwargs):
3505
3549
  global __global_suppress_printout
3506
3550
  global __DEBUG_MODE
3507
3551
  try:
3508
- history_file = os.path.expanduser(kwargs.get('history_file', DEFAULT_HISTORY_FILE))
3509
- import inspect
3510
- sig = inspect.signature(getStrCommand)
3511
- wanted = {
3512
- name: kwargs[name]
3513
- for name in sig.parameters
3514
- if name in kwargs
3515
- }
3516
- strCommand = getStrCommand(**wanted,shortend=True,tabSeperated=True)
3552
+ historyKwargs = dict(kwargs)
3553
+ historyKwargs['shortend'] = True
3554
+ historyKwargs['tabSeperated'] = True
3555
+ history_file = os.path.expanduser(historyKwargs.get('history_file', DEFAULT_HISTORY_FILE))
3556
+ strCommand = getStrCommand(**historyKwargs)
3517
3557
  with open(history_file, 'a') as f:
3518
3558
  # it follows <timestamp>\t<strCommand>\n
3519
3559
  f.write(f'{int(time.time())}\t{strCommand}\n')
@@ -3869,10 +3909,13 @@ def generate_default_config(args):
3869
3909
  'DEFAULT_INTERVAL': args.interval,
3870
3910
  'DEFAULT_IPMI': args.ipmi,
3871
3911
  'DEFAULT_IPMI_INTERFACE_IP_PREFIX': args.ipmi_interface_ip_prefix,
3872
- 'DEFAULT_INTERFACE_IP_PREFIX': args.interface_ip_prefix,
3873
3912
  'DEFAULT_IPMI_USERNAME': args.ipmi_username,
3874
3913
  'DEFAULT_IPMI_PASSWORD': args.ipmi_password,
3875
3914
  'DEAFULT_IPMI_ARGS': args.ipmi_args,
3915
+ 'DEFAULT_IPMI_METHOD': args.ipmi_method,
3916
+ 'DEFAULT_IPMI_RETRY_LOCKOUT_TIME': args.ipmi_retry_lockout_time,
3917
+ 'DEFAULT_INTERFACE_IP_PREFIX': args.interface_ip_prefix,
3918
+ 'DEFAULT_IPMI_DEFINITIONS': DEFAULT_IPMI_DEFINITIONS,
3876
3919
  'DEFAULT_NO_WATCH': args.no_watch,
3877
3920
  'DEFAULT_WINDOW_WIDTH': args.window_width,
3878
3921
  'DEFAULT_WINDOW_HEIGHT': args.window_height,
@@ -3893,8 +3936,6 @@ def generate_default_config(args):
3893
3936
  'DEFAULT_HOST_FILE': args.host_file,
3894
3937
  'DEFAULT_ENCODING': args.encoding,
3895
3938
  'DEFAULT_DIFF_DISPLAY_THRESHOLD': args.diff_display_threshold,
3896
- 'SSH_STRICT_HOST_KEY_CHECKING': SSH_STRICT_HOST_KEY_CHECKING,
3897
- 'ERROR_MESSAGES_TO_IGNORE': ERROR_MESSAGES_TO_IGNORE,
3898
3939
  'FORCE_TRUECOLOR': args.force_truecolor,
3899
3940
  }
3900
3941
 
@@ -3939,16 +3980,24 @@ def write_default_config(args,CONFIG_FILE = None, force = False):
3939
3980
 
3940
3981
  #%% ------------ Argument Processing -----------------
3941
3982
  def get_parser():
3942
- global _binPaths
3943
- parser = argparse.ArgumentParser(description='Run a command on multiple hosts, Use #HOST# or #HOSTNAME# to replace the host name in the command.',
3944
- epilog=f'Found bins: {list(_binPaths.values())}\n Missing bins: {_binCalled - set(_binPaths.keys())}\n Terminal color capability: {get_terminal_color_capability()}\nConfig file chain: {CONFIG_FILE_CHAIN!r}',)
3983
+ parser = argparse.ArgumentParser(
3984
+ description='Run a command on multiple hosts, Use #HOST# or #HOSTNAME# to replace the host name in the command.',
3985
+ epilog=(
3986
+ f"Terminal color capability: {get_terminal_color_capability()}\n"
3987
+ f"Found bins: \n{pformat(set(_binPaths.values()))}\n"
3988
+ f"Missing bins: \n{pformat(_binCalled - set(_binPaths.keys()))}\n"
3989
+ f"Config file chain: \n{pformat(CONFIG_FILE_CHAIN)}\n"
3990
+ f"Hosts Env files searched:\n{pformat(DEFAULT_ENV_FILES)}"
3991
+ ),
3992
+ formatter_class=argparse.RawTextHelpFormatter,
3993
+ )
3945
3994
  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)
3946
3995
  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.')
3947
- 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)
3948
- parser.add_argument('-p', '--password', type=str,help=f'The password to use to connect to the hosts, (default: {DEFAULT_PASSWORD})',default=DEFAULT_PASSWORD)
3996
+ parser.add_argument('-u','--username', type=str,help=f'The general username to use to connect to the hosts via ssh. Will get overwrote by individual username@host if specified. (default: {DEFAULT_USERNAME})',default=DEFAULT_USERNAME)
3997
+ parser.add_argument('-p', '--password', type=str,help=f'The password to use to connect to the hosts via ssh, (default: {DEFAULT_PASSWORD})',default=DEFAULT_PASSWORD)
3949
3998
  parser.add_argument('-k','--identity_file','--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 {DEFAULT_SSH_KEY_SEARCH_PATH} (default: {DEFAULT_IDENTITY_FILE})',const=DEFAULT_SSH_KEY_SEARCH_PATH,default=DEFAULT_IDENTITY_FILE)
3950
3999
  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)
3951
- 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)
4000
+ parser.add_argument('-ea','--extraargs',type=str,help=f'Extra arguments to pass to the ssh / rsync / scp command. (Use -ia for ipmi arguments) Put in one string for multiple arguments.Use "=" ! Ex. -ea="--delete" (default: {DEFAULT_EXTRA_ARGS})',default=DEFAULT_EXTRA_ARGS)
3952
4001
  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)
3953
4002
  parser.add_argument("-f","--file", action='append', help="The file to be copied to the hosts. Use -f multiple times to copy multiple files")
3954
4003
  parser.add_argument('-s','-fs','--file_sync',nargs='?', action='append', help=f'Operate in file sync mode, sync path in <COMMANDS> from this machine to <HOSTS>. Treat --file <FILE> and <COMMANDS> both as source and source and destination will be the same in this mode. Infer destination from source path. (default: {DEFAULT_FILE_SYNC})',const=True, default=[DEFAULT_FILE_SYNC])
@@ -3964,7 +4013,9 @@ def get_parser():
3964
4013
  parser.add_argument("-pre","--interface_ip_prefix", type=str, help=f"The prefix of the for the interfaces (default: {DEFAULT_INTERFACE_IP_PREFIX})", default=DEFAULT_INTERFACE_IP_PREFIX)
3965
4014
  parser.add_argument('-iu','--ipmi_username', type=str,help=f'The username to use to connect to the hosts via ipmi. (default: {DEFAULT_IPMI_USERNAME})',default=DEFAULT_IPMI_USERNAME)
3966
4015
  parser.add_argument('-ip','--ipmi_password', type=str,help=f'The password to use to connect to the hosts via ipmi. (default: {DEFAULT_IPMI_PASSWORD})',default=DEFAULT_IPMI_PASSWORD)
3967
- parser.add_argument('-ia','--ipmi_args',type=str,help=f'The arguments passed to ipmitool to use to connect to the hosts via ipmi. (default: {DEAFULT_IPMI_ARGS})',default=DEAFULT_IPMI_ARGS)
4016
+ parser.add_argument('-ia','--ipmi_args',type=str,help=f'The arguments passed to ipmitool to use to connect to the hosts via ipmi. (default: {DEFAULT_IPMI_ARGS})',default=DEFAULT_IPMI_ARGS)
4017
+ parser.add_argument('-im','--ipmi_method', type=str,help=f'The method to use to connect to the hosts via ipmi. (default: {DEFAULT_IPMI_METHOD})',default=DEFAULT_IPMI_METHOD)
4018
+ parser.add_argument('-irlt','--ipmi_retry_lockout_time', type=int,help=f'The time in seconds to retry the lockout of the hosts via ipmi. (default: {DEFAULT_IPMI_RETRY_LOCKOUT_TIME})',default=DEFAULT_IPMI_RETRY_LOCKOUT_TIME)
3968
4019
  parser.add_argument('-S',"-q","-nw","--no_watch", action='store_true', help=f"Quiet mode, no curses watch, only print the output. (default: {DEFAULT_NO_WATCH})", default=DEFAULT_NO_WATCH)
3969
4020
  parser.add_argument("-ww",'--window_width', type=int, help=f"The minimum character length of the curses window. (default: {DEFAULT_WINDOW_WIDTH})", default=DEFAULT_WINDOW_WIDTH)
3970
4021
  parser.add_argument("-wh",'--window_height', type=int, help=f"The minimum line height of the curses window. (default: {DEFAULT_WINDOW_HEIGHT})", default=DEFAULT_WINDOW_HEIGHT)
@@ -4001,8 +4052,6 @@ def get_parser():
4001
4052
  return parser
4002
4053
 
4003
4054
  def process_args(args = None):
4004
- global DEFAULT_IPMI_USERNAME
4005
- global DEFAULT_IPMI_PASSWORD
4006
4055
  global __configs_from_file
4007
4056
  cfp = parser = argparse.ArgumentParser(add_help=False)
4008
4057
  cfp.add_argument('--config_file', type=str,nargs='?',const='multiSSH3.config.json',default=None)
@@ -4060,7 +4109,27 @@ def process_args(args = None):
4060
4109
  if args.unavailable_host_expiry <= 0:
4061
4110
  eprint(f"Warning: The unavailable host expiry time {args.unavailable_host_expiry} is less than 0, setting it to 10 seconds.")
4062
4111
  args.unavailable_host_expiry = 10
4063
-
4112
+ default_ipmi_definition = get_default_ipmi_definition()
4113
+ while DEFAULT_IPMI_DEFINITIONS:
4114
+ last_definition = DEFAULT_IPMI_DEFINITIONS.pop()
4115
+ if last_definition != default_ipmi_definition:
4116
+ DEFAULT_IPMI_DEFINITIONS.append(last_definition)
4117
+ break
4118
+ fields = [
4119
+ ('interface_ip_prefix', args.ipmi_interface_ip_prefix),
4120
+ ('username', args.ipmi_username),
4121
+ ('password', args.ipmi_password),
4122
+ ('args', args.ipmi_args),
4123
+ ('ipmi_method', args.ipmi_method),
4124
+ ('retry_lockout_time', args.ipmi_retry_lockout_time),
4125
+ ]
4126
+ modified = any(val != default_ipmi_definition[key] for key, val in fields)
4127
+ for key, val in fields:
4128
+ default_ipmi_definition[key] = val
4129
+ if modified:
4130
+ DEFAULT_IPMI_DEFINITIONS.insert(0, default_ipmi_definition)
4131
+ else:
4132
+ DEFAULT_IPMI_DEFINITIONS.append(default_ipmi_definition)
4064
4133
  return args
4065
4134
 
4066
4135
  def process_config_file(args):
@@ -4143,19 +4212,14 @@ def process_control_master_config(args):
4143
4212
 
4144
4213
  def set_global_with_args(args):
4145
4214
  global _emo
4146
- global __ipmiiInterfaceIPPrefix
4147
4215
  global _env_files
4148
4216
  global __DEBUG_MODE
4149
4217
  global _encoding
4150
4218
  global __returnZero
4151
- global DEFAULT_IPMI_USERNAME
4152
- global DEFAULT_IPMI_PASSWORD
4153
4219
  global DEFAULT_DIFF_DISPLAY_THRESHOLD
4154
4220
  global FORCE_TRUECOLOR
4155
4221
  global DEFAULT_HOST_FILE
4156
- global __ipmi_args
4157
4222
  _emo = False
4158
- __ipmiiInterfaceIPPrefix = args.ipmi_interface_ip_prefix
4159
4223
  if args.env_file:
4160
4224
  _env_files = [args.env_file]
4161
4225
  else:
@@ -4164,15 +4228,10 @@ def set_global_with_args(args):
4164
4228
  _encoding = args.encoding
4165
4229
  if args.return_zero:
4166
4230
  __returnZero = True
4167
- if args.ipmi_username:
4168
- DEFAULT_IPMI_USERNAME = args.ipmi_username
4169
- if args.ipmi_password:
4170
- DEFAULT_IPMI_PASSWORD = args.ipmi_password
4171
4231
  DEFAULT_DIFF_DISPLAY_THRESHOLD = args.diff_display_threshold
4172
4232
  FORCE_TRUECOLOR = args.force_truecolor
4173
4233
  if args.host_file:
4174
4234
  DEFAULT_HOST_FILE = args.host_file
4175
- __ipmi_args = args.ipmi_args
4176
4235
 
4177
4236
  #%% ------------ Wrapper Block ----------------
4178
4237
  def main(args = None):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes