multiSSH3 6.24__tar.gz → 6.25__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.
Files changed (35) hide show
  1. {multissh3-6.24 → multissh3-6.25}/PKG-INFO +1 -1
  2. {multissh3-6.24 → multissh3-6.25}/multiSSH3.egg-info/PKG-INFO +1 -1
  3. {multissh3-6.24 → multissh3-6.25}/multiSSH3.egg-info/SOURCES.txt +1 -0
  4. {multissh3-6.24 → multissh3-6.25}/multiSSH3.egg-info/dependency_links.txt +0 -0
  5. {multissh3-6.24 → multissh3-6.25}/multiSSH3.egg-info/entry_points.txt +0 -0
  6. {multissh3-6.24 → multissh3-6.25}/multiSSH3.egg-info/requires.txt +0 -0
  7. {multissh3-6.24 → multissh3-6.25}/multiSSH3.egg-info/top_level.txt +0 -0
  8. {multissh3-6.24 → multissh3-6.25}/multiSSH3.py +55 -2
  9. multissh3-6.25/tests/test_ipmi_lockout_wait.py +32 -0
  10. {multissh3-6.24 → multissh3-6.25}/README.md +0 -0
  11. {multissh3-6.24 → multissh3-6.25}/setup.cfg +0 -0
  12. {multissh3-6.24 → multissh3-6.25}/setup.py +0 -0
  13. {multissh3-6.24 → multissh3-6.25}/tests/test_cli_branches.py +0 -0
  14. {multissh3-6.24 → multissh3-6.25}/tests/test_cli_processing.py +0 -0
  15. {multissh3-6.24 → multissh3-6.25}/tests/test_color_helpers.py +0 -0
  16. {multissh3-6.24 → multissh3-6.25}/tests/test_compact_hostnames.py +0 -0
  17. {multissh3-6.24 → multissh3-6.25}/tests/test_config_cli.py +0 -0
  18. {multissh3-6.24 → multissh3-6.25}/tests/test_config_validate.py +0 -0
  19. {multissh3-6.24 → multissh3-6.25}/tests/test_curses_branches.py +0 -0
  20. {multissh3-6.24 → multissh3-6.25}/tests/test_curses_event_loop.py +0 -0
  21. {multissh3-6.24 → multissh3-6.25}/tests/test_curses_tui.py +0 -0
  22. {multissh3-6.24 → multissh3-6.25}/tests/test_expand_hostnames.py +0 -0
  23. {multissh3-6.24 → multissh3-6.25}/tests/test_harness_smoke.py +0 -0
  24. {multissh3-6.24 → multissh3-6.25}/tests/test_helpers_extra.py +0 -0
  25. {multissh3-6.24 → multissh3-6.25}/tests/test_host.py +0 -0
  26. {multissh3-6.24 → multissh3-6.25}/tests/test_main_lifecycle.py +0 -0
  27. {multissh3-6.24 → multissh3-6.25}/tests/test_ordered_multiset.py +0 -0
  28. {multissh3-6.24 → multissh3-6.25}/tests/test_output_merge.py +0 -0
  29. {multissh3-6.24 → multissh3-6.25}/tests/test_run_command.py +0 -0
  30. {multissh3-6.24 → multissh3-6.25}/tests/test_run_command_branches.py +0 -0
  31. {multissh3-6.24 → multissh3-6.25}/tests/test_run_on_hosts.py +0 -0
  32. {multissh3-6.24 → multissh3-6.25}/tests/test_run_on_hosts_modes.py +0 -0
  33. {multissh3-6.24 → multissh3-6.25}/tests/test_runtime_branch_matrix.py +0 -0
  34. {multissh3-6.24 → multissh3-6.25}/tests/test_smoke_optional.py +0 -0
  35. {multissh3-6.24 → multissh3-6.25}/tests/test_unavailable_hosts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 6.24
3
+ Version: 6.25
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.24
3
+ Version: 6.25
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,6 +20,7 @@ tests/test_expand_hostnames.py
20
20
  tests/test_harness_smoke.py
21
21
  tests/test_helpers_extra.py
22
22
  tests/test_host.py
23
+ tests/test_ipmi_lockout_wait.py
23
24
  tests/test_main_lifecycle.py
24
25
  tests/test_ordered_multiset.py
25
26
  tests/test_output_merge.py
@@ -36,7 +36,7 @@ from itertools import count, product
36
36
  from pprint import pformat
37
37
 
38
38
 
39
- version = '6.24'
39
+ version = '6.25'
40
40
  VERSION = version
41
41
  __version__ = version
42
42
  COMMIT_DATE = '2026-08-11'
@@ -1861,6 +1861,56 @@ def __handle_writing_stream(stream,stop_event,host):
1861
1861
  def _emfile_backoff_seconds(attempt):
1862
1862
  return min(0.1 * (2 ** attempt), 1.0)
1863
1863
 
1864
+ def _ipmi_failure_should_wait(host):
1865
+ '''Return True when the next IPMI password should wait out retry_lockout_time.
1866
+
1867
+ The wait protects a controller that actually rejected a login. Timeouts, routing
1868
+ failures, and a socket that never opened did not reach a BMC, so they skip it.
1869
+ An unrecognized BMC error still waits.
1870
+ '''
1871
+ chunks = []
1872
+ for attr in ('stdout', 'stderr', 'output'):
1873
+ value = getattr(host, attr, None) or []
1874
+ chunks.extend(str(line) for line in value)
1875
+ text = '\n'.join(chunks).lower()
1876
+ auth_markers = (
1877
+ 'insufficient resources',
1878
+ 'rakp',
1879
+ 'unauthorized',
1880
+ 'authentication failed',
1881
+ 'invalid password',
1882
+ 'invalid user',
1883
+ 'bad password',
1884
+ 'account locked',
1885
+ 'user locked',
1886
+ 'session limit',
1887
+ 'privilege level',
1888
+ )
1889
+ if any(marker in text for marker in auth_markers):
1890
+ return True
1891
+ no_contact_markers = (
1892
+ 'could not open socket',
1893
+ 'no route to host',
1894
+ 'network is unreachable',
1895
+ 'connection timed out',
1896
+ 'connection refused',
1897
+ 'host is down',
1898
+ 'destination host unreachable',
1899
+ 'name or service not known',
1900
+ 'could not resolve',
1901
+ 'permission denied',
1902
+ 'unable to establish lan session',
1903
+ 'unable to establish ipmi v1.5',
1904
+ 'timeout!',
1905
+ )
1906
+ if any(marker in text for marker in no_contact_markers):
1907
+ return False
1908
+ if getattr(host, 'returncode', None) == 124:
1909
+ return False
1910
+ if 'unable to establish ipmi v2' in text or 'rmcp+' in text:
1911
+ return True
1912
+ return True
1913
+
1864
1914
  def run_command(host, sem, timeout=60,passwds=None, retry_limit = 7 + len(DEFAULT_IPMI_DEFINITIONS),ipmi_definitions_list = ...):
1865
1915
  '''
1866
1916
  Run the command on the host. Will format the commands accordingly. Main execution function.
@@ -2199,6 +2249,7 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 7 + len(DEFAUL
2199
2249
  retry_limit -= 1
2200
2250
  # If using ipmi, we will try again using next ipmi definition if ipmi connection is not successful
2201
2251
  if host.ipmi and host.returncode != 0:
2252
+ should_wait = _ipmi_failure_should_wait(host)
2202
2253
  host.returncode = None
2203
2254
  host.output.append('IPMI connection failed! Trying next ipmi definition...')
2204
2255
  if __DEBUG_MODE:
@@ -2209,11 +2260,13 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 7 + len(DEFAUL
2209
2260
  if ipmi_definition.get('retry_lockout_time'):
2210
2261
  try:
2211
2262
  sleep_seconds = int(ipmi_definition.get('retry_lockout_time'))
2212
- if sleep_seconds >= 0:
2263
+ if sleep_seconds >= 0 and should_wait:
2213
2264
  host.output.append(f'Sleeping for {sleep_seconds} seconds...')
2214
2265
  if __DEBUG_MODE:
2215
2266
  host.stderr.append(f'Sleeping for {sleep_seconds} seconds...')
2216
2267
  time.sleep(sleep_seconds)
2268
+ elif sleep_seconds >= 0:
2269
+ host.output.append('Skipping IPMI lockout wait; the controller was not reached.')
2217
2270
  except Exception as e:
2218
2271
  host.output.append(f'Error: Failed to sleep for {ipmi_definition.get("retry_lockout_time")} seconds! {e}')
2219
2272
  if __DEBUG_MODE:
@@ -0,0 +1,32 @@
1
+ import multiSSH3
2
+
3
+
4
+ class _Host:
5
+ def __init__(self, stderr, returncode=1):
6
+ self.stdout = []
7
+ self.stderr = stderr
8
+ self.output = []
9
+ self.returncode = returncode
10
+
11
+
12
+ def test_ipmi_lockout_wait_skips_when_bmc_was_not_reached():
13
+ cases = [
14
+ ["Could not open socket!", "Error: Unable to establish IPMI v2 / RMCP+ session"],
15
+ ["Error: Unable to establish LAN session", "Error: Unable to establish IPMI v1.5 / RMCP session"],
16
+ ["Timeout!"],
17
+ ["ssh: connect to host 10.251.5.57 port 22: Connection timed out"],
18
+ ["Permission denied, please try again."],
19
+ ]
20
+ for stderr in cases:
21
+ assert multiSSH3._ipmi_failure_should_wait(_Host(stderr, 124 if stderr == ["Timeout!"] else 1)) is False
22
+
23
+
24
+ def test_ipmi_lockout_wait_runs_after_bmc_auth_failure():
25
+ cases = [
26
+ ["RAKP 2 HMAC is invalid", "Error: Unable to establish IPMI v2 / RMCP+ session"],
27
+ ["RAKP 2 message indicates an error : insufficient resources for session"],
28
+ ["Error: Unable to establish IPMI v2 / RMCP+ session"],
29
+ ["Set Session Privilege Level to ADMINISTRATOR failed"],
30
+ ]
31
+ for stderr in cases:
32
+ assert multiSSH3._ipmi_failure_should_wait(_Host(stderr)) is True
File without changes
File without changes
File without changes
File without changes