multiSSH3 5.36__py3-none-any.whl → 5.40__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
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: multiSSH3
3
- Version: 5.36
3
+ Version: 5.40
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
@@ -13,6 +13,15 @@ Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: argparse
15
15
  Requires-Dist: ipaddress
16
+ Dynamic: author
17
+ Dynamic: author-email
18
+ Dynamic: classifier
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: home-page
22
+ Dynamic: requires-dist
23
+ Dynamic: requires-python
24
+ Dynamic: summary
16
25
 
17
26
  # multiSSH3
18
27
  A script that is able to issue commands to multiple hosts while monitoring their progress.
@@ -43,6 +52,15 @@ mssh --store_config_file
43
52
 
44
53
  You can modify the json file directly after generation and multissh will read from it for loading defaults.
45
54
 
55
+ ```bash
56
+ mssh --ipmi_interface_ip_prefix 192 --store_config_file
57
+ ```
58
+ will store
59
+ ```json
60
+ "DEFAULT_IPMI_INTERFACE_IP_PREFIX": "192"
61
+ ```
62
+ into the json file.
63
+
46
64
  Note:
47
65
 
48
66
  If you want to store password, it will be a plain text password in this config file. This will be better to supply it everytime as a CLI argument but you should really consider setting up priv-pub key setup.
@@ -53,15 +71,6 @@ On some systems, scp / rsync will require you use a priv-pub key to work
53
71
 
54
72
  This option can also be used to store cli options into the config files. For example.
55
73
 
56
- ```bash
57
- mssh --ipmi_interface_ip_prefix 192 --store_config_file
58
- ```
59
- will store
60
- ```json
61
- "DEFAULT_IPMI_INTERFACE_IP_PREFIX": "192"
62
- ```
63
- into the json file.
64
-
65
74
  By defualt reads bash env variables for hostname aliases. Also able to read
66
75
  ```bash
67
76
  DEFAULT_ENV_FILE = '/etc/profile.d/hosts.sh'
@@ -77,7 +86,7 @@ us_east='100.100.0.1-3,us_east_prod_[1-5]'
77
86
  us_central=""
78
87
  us_west="100.101.0.1-2,us_west_prod_[a-c]_[1-3]"
79
88
  us="$us_east,$us_central,$us_west"
80
- asia="100.90.0-1,1-9"
89
+ asia="100.90.0-1.1-9"
81
90
  eu=''
82
91
  rhel8="$asia,$us_east"
83
92
  all="$us,$asia,$eu"
@@ -0,0 +1,7 @@
1
+ multiSSH3.py,sha256=X9lJZNiWAuiNtEnSjBkqSgbgGMWhxB5MZIjS_ejpj14,137577
2
+ multiSSH3-5.40.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
3
+ multiSSH3-5.40.dist-info/METADATA,sha256=pJGmT8HeakwctZYc4uW54_Z-v2Q1liCL1TjD7xddV5E,18366
4
+ multiSSH3-5.40.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
5
+ multiSSH3-5.40.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
6
+ multiSSH3-5.40.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
7
+ multiSSH3-5.40.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.6.0)
2
+ Generator: setuptools (75.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
multiSSH3.py CHANGED
@@ -1,13 +1,21 @@
1
1
  #!/usr/bin/env python3
2
2
  __curses_available = False
3
+ __resource_lib_available = False
3
4
  try:
4
5
  import curses
5
6
  __curses_available = True
6
7
  except ImportError:
7
8
  pass
9
+ try:
10
+ import resource
11
+ __resource_lib_available = True
12
+ except ImportError:
13
+ pass
14
+
8
15
  import subprocess
9
16
  import threading
10
- import time,os
17
+ import time
18
+ import os
11
19
  import argparse
12
20
  from itertools import product
13
21
  import re
@@ -37,7 +45,7 @@ except AttributeError:
37
45
  # If neither is available, use a dummy decorator
38
46
  def cache_decorator(func):
39
47
  return func
40
- version = '5.36'
48
+ version = '5.40'
41
49
  VERSION = version
42
50
 
43
51
  CONFIG_FILE = '/etc/multiSSH3.config.json'
@@ -158,7 +166,7 @@ def _get_i():
158
166
 
159
167
  # ------------ Host Object ----------------
160
168
  class Host:
161
- def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None,gatherMode=False,identity_file=None,bash=False,i = _get_i(),uuid=uuid.uuid4(),ip = None):
169
+ 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 = _get_i(),uuid=uuid.uuid4(),ip = None):
162
170
  self.name = name # the name of the host (hostname or IP address)
163
171
  self.command = command # the command to run on the host
164
172
  self.returncode = None # the return code of the command
@@ -170,7 +178,7 @@ class Host:
170
178
  self.lastUpdateTime = time.time() # the last time the output was updated
171
179
  self.files = files # the files to be copied to the host
172
180
  self.ipmi = ipmi # whether to use ipmi to connect to the host
173
- self.bash = bash # whether to use bash to run the command
181
+ self.shell = shell # whether to use shell to run the command
174
182
  self.interface_ip_prefix = interface_ip_prefix # the prefix of the ip address of the interface to be used to connect to the host
175
183
  self.scp = scp # whether to use scp to copy files to the host
176
184
  self.gatherMode = gatherMode # whether the host is in gather mode
@@ -272,7 +280,7 @@ __build_in_default_config = {
272
280
  '_scpPath': None,
273
281
  '_ipmitoolPath': None,
274
282
  '_rsyncPath': None,
275
- '_bashPath': None,
283
+ '_shellPath': None,
276
284
  '__ERROR_MESSAGES_TO_IGNORE_REGEX':None,
277
285
  '__DEBUG_MODE': False,
278
286
  }
@@ -352,26 +360,34 @@ if True:
352
360
  __curses_current_color_pair_index = 2 # Start from 1, as 0 is the default color pair
353
361
  __curses_color_table = {}
354
362
  __curses_current_color_index = 10
363
+ __max_connections_nofile_limit_supported = 0
364
+ if __resource_lib_available:
365
+ # Get the current limits
366
+ _, __system_nofile_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
367
+ # Set the soft limit to the hard limit
368
+ resource.setrlimit(resource.RLIMIT_NOFILE, (__system_nofile_limit, __system_nofile_limit))
369
+ __max_connections_nofile_limit_supported = int((__system_nofile_limit - 5) / 4.6)
355
370
 
356
371
  # Mapping of ANSI 4-bit colors to curses colors
357
- ANSI_TO_CURSES_COLOR = {
358
- 30: curses.COLOR_BLACK,
359
- 31: curses.COLOR_RED,
360
- 32: curses.COLOR_GREEN,
361
- 33: curses.COLOR_YELLOW,
362
- 34: curses.COLOR_BLUE,
363
- 35: curses.COLOR_MAGENTA,
364
- 36: curses.COLOR_CYAN,
365
- 37: curses.COLOR_WHITE,
366
- 90: curses.COLOR_BLACK, # Bright Black (usually gray)
367
- 91: curses.COLOR_RED, # Bright Red
368
- 92: curses.COLOR_GREEN, # Bright Green
369
- 93: curses.COLOR_YELLOW, # Bright Yellow
370
- 94: curses.COLOR_BLUE, # Bright Blue
371
- 95: curses.COLOR_MAGENTA, # Bright Magenta
372
- 96: curses.COLOR_CYAN, # Bright Cyan
373
- 97: curses.COLOR_WHITE # Bright White
374
- }
372
+ if __curses_available:
373
+ ANSI_TO_CURSES_COLOR = {
374
+ 30: curses.COLOR_BLACK,
375
+ 31: curses.COLOR_RED,
376
+ 32: curses.COLOR_GREEN,
377
+ 33: curses.COLOR_YELLOW,
378
+ 34: curses.COLOR_BLUE,
379
+ 35: curses.COLOR_MAGENTA,
380
+ 36: curses.COLOR_CYAN,
381
+ 37: curses.COLOR_WHITE,
382
+ 90: curses.COLOR_BLACK, # Bright Black (usually gray)
383
+ 91: curses.COLOR_RED, # Bright Red
384
+ 92: curses.COLOR_GREEN, # Bright Green
385
+ 93: curses.COLOR_YELLOW, # Bright Yellow
386
+ 94: curses.COLOR_BLUE, # Bright Blue
387
+ 95: curses.COLOR_MAGENTA, # Bright Magenta
388
+ 96: curses.COLOR_CYAN, # Bright Cyan
389
+ 97: curses.COLOR_WHITE # Bright White
390
+ }
375
391
  # ------------ Exportable Help Functions ----------------
376
392
  # check if command sshpass is available
377
393
  _binPaths = {}
@@ -390,7 +406,7 @@ def check_path(program_name):
390
406
  return True
391
407
  return False
392
408
 
393
- [check_path(program) for program in ['sshpass', 'ssh', 'scp', 'ipmitool','rsync','bash','ssh-copy-id']]
409
+ [check_path(program) for program in ['sshpass', 'ssh', 'scp', 'ipmitool','rsync','sh','ssh-copy-id']]
394
410
 
395
411
  def find_ssh_key_file(searchPath = DEDAULT_SSH_KEY_SEARCH_PATH):
396
412
  '''
@@ -472,6 +488,7 @@ def replace_magic_strings(string,keys,value,case_sensitive=False):
472
488
  return string
473
489
 
474
490
  def pretty_format_table(data):
491
+ version = 1.0
475
492
  if not data:
476
493
  return ''
477
494
  if type(data) == str:
@@ -488,7 +505,6 @@ def pretty_format_table(data):
488
505
  data = [[key] + list(value) for key, value in data.items()]
489
506
  elif type(data) != list:
490
507
  data = list(data)
491
- # TODO : add support for list of dictionaries
492
508
  # format the list into 2d list of list of strings
493
509
  if isinstance(data[0], dict):
494
510
  tempData = [data[0].keys()]
@@ -833,7 +849,7 @@ def __compact_hostnames(Hostnames):
833
849
  rtnSet.add(''.join(hostnameList))
834
850
  return frozenset(rtnSet)
835
851
 
836
- def compact_hostnames(Hostnames):
852
+ def compact_hostnames(Hostnames,verify = True):
837
853
  """
838
854
  Compact a list of hostnames.
839
855
  Compact numeric numbers into ranges.
@@ -864,10 +880,11 @@ def compact_hostnames(Hostnames):
864
880
  else:
865
881
  hostSet = Hostnames
866
882
  compact_hosts = __compact_hostnames(hostSet)
867
- if set(expand_hostnames(compact_hosts)) != set(expand_hostnames(hostSet)):
868
- if not __global_suppress_printout:
869
- eprint(f"Error compacting hostnames: {hostSet} -> {compact_hosts}")
870
- compact_hosts = hostSet
883
+ if verify:
884
+ if set(expand_hostnames(compact_hosts)) != set(expand_hostnames(hostSet)):
885
+ if not __global_suppress_printout:
886
+ eprint(f"Error compacting hostnames: {hostSet} -> {compact_hosts}")
887
+ compact_hosts = hostSet
871
888
  return compact_hosts
872
889
 
873
890
  # ------------ Expanding Hostnames ----------------
@@ -883,7 +900,6 @@ def __validate_expand_hostname(hostname):
883
900
  list: A list of valid hostnames
884
901
  '''
885
902
  global _no_env
886
- # maybe it is just defined in ./target_files/hosts.sh and exported to the environment
887
903
  # we will try to get the valid host name from the environment
888
904
  hostname = hostname.strip().strip('$')
889
905
  if getIP(hostname,local=True):
@@ -1018,6 +1034,8 @@ def __expand_hostnames(hosts) -> dict:
1018
1034
  dict: A dictionary of expanded hostnames with key: hostname, value: resolved IP address
1019
1035
  '''
1020
1036
  expandedhosts = {}
1037
+ if isinstance(hosts, str):
1038
+ hosts = [hosts]
1021
1039
  for host in hosts:
1022
1040
  host = host.strip()
1023
1041
  if not host:
@@ -1220,12 +1238,12 @@ def run_command(host, sem, timeout=60,passwds=None):
1220
1238
  host.username = 'admin'
1221
1239
  if not host.command:
1222
1240
  host.command = 'power status'
1223
- if 'bash' in _binPaths:
1241
+ if 'sh' in _binPaths:
1224
1242
  if passwds:
1225
- formatedCMD = [_binPaths['bash'],'-c',f'ipmitool -H {host.address} -U {host.username} -P {passwds} {" ".join(extraargs)} {host.command}']
1243
+ formatedCMD = [_binPaths['sh'],'-c',f'ipmitool -H {host.address} -U {host.username} -P {passwds} {" ".join(extraargs)} {host.command}']
1226
1244
  else:
1227
1245
  host.output.append('Warning: Password not provided for ipmi! Using a default password `admin`.')
1228
- formatedCMD = [_binPaths['bash'],'-c',f'ipmitool -H {host.address} -U {host.username} -P admin {" ".join(extraargs)} {host.command}']
1246
+ formatedCMD = [_binPaths['sh'],'-c',f'ipmitool -H {host.address} -U {host.username} -P admin {" ".join(extraargs)} {host.command}']
1229
1247
  else:
1230
1248
  if passwds:
1231
1249
  formatedCMD = [_binPaths['ipmitool'],f'-H {host.address}',f'-U {host.username}',f'-P {passwds}'] + extraargs + [host.command]
@@ -1249,17 +1267,17 @@ def run_command(host, sem, timeout=60,passwds=None):
1249
1267
  host.stderr.append('Ipmitool not found on the local machine! Please install ipmitool to use ipmi mode.')
1250
1268
  host.returncode = 1
1251
1269
  return
1252
- elif host.bash:
1253
- if 'bash' in _binPaths:
1254
- host.output.append('Running command in bash mode, ignoring the hosts...')
1270
+ elif host.shell:
1271
+ if 'sh' in _binPaths:
1272
+ host.output.append('Running command in shell mode, ignoring the hosts...')
1255
1273
  if __DEBUG_MODE:
1256
- host.stderr.append('Running command in bash mode, ignoring the hosts...')
1257
- formatedCMD = [_binPaths['bash'],'-c',host.command]
1274
+ host.stderr.append('Running command in shell mode, ignoring the hosts...')
1275
+ formatedCMD = [_binPaths['sh'],'-c',host.command]
1258
1276
  else:
1259
- host.output.append('Bash not found on the local machine! Using ssh localhost instead...')
1277
+ host.output.append('shell not found on the local machine! Using ssh localhost instead...')
1260
1278
  if __DEBUG_MODE:
1261
- host.stderr.append('Bash not found on the local machine! Using ssh localhost instead...')
1262
- host.bash = False
1279
+ host.stderr.append('shell not found on the local machine! Using ssh localhost instead...')
1280
+ host.shell = False
1263
1281
  host.name = 'localhost'
1264
1282
  run_command(host,sem,timeout,passwds)
1265
1283
  else:
@@ -1547,6 +1565,7 @@ def __get_curses_color_pair(fg, bg):
1547
1565
  if __curses_current_color_pair_index >= curses.COLOR_PAIRS:
1548
1566
  print("Warning: Maximum number of color pairs reached, wrapping around.")
1549
1567
  __curses_current_color_pair_index = 1
1568
+ # TODO: avoid initializing the same fg and bg color
1550
1569
  curses.init_pair(__curses_current_color_pair_index, fg, bg)
1551
1570
  __curses_global_color_pairs[(fg, bg)] = __curses_current_color_pair_index
1552
1571
  __curses_current_color_pair_index += 1
@@ -1725,8 +1744,8 @@ def _curses_add_string_to_window(window, line = '', y = 0, x = 0, number_of_char
1725
1744
  # process centering
1726
1745
  if centered:
1727
1746
  fill_length = numChar - len(lead_str) - len(trail_str) - sum([len(segment) for segment in segments if not segment.startswith("\x1b[")])
1728
- window.addnstr(y, x + charsWritten, fill_char * (fill_length // 2), numChar - charsWritten, boxAttr)
1729
- charsWritten += min(fill_length // 2, numChar - charsWritten)
1747
+ window.addnstr(y, x + charsWritten, fill_char * (fill_length // 2 // len(fill_char)), numChar - charsWritten, boxAttr)
1748
+ charsWritten += min(len(fill_char * (fill_length // 2)), numChar - charsWritten)
1730
1749
  # add the segments
1731
1750
  for segment in segments:
1732
1751
  if not segment:
@@ -1741,7 +1760,7 @@ def _curses_add_string_to_window(window, line = '', y = 0, x = 0, number_of_char
1741
1760
  charsWritten += min(len(segment), numChar - charsWritten)
1742
1761
  # if we have finished printing segments but we still have space, we will fill it with fill_char
1743
1762
  if charsWritten + len(trail_str) < numChar:
1744
- fillStr = fill_char * (numChar - charsWritten - len(trail_str))
1763
+ fillStr = fill_char * ((numChar - charsWritten - len(trail_str))//len(fill_char))
1745
1764
  #fillStr = f'{color_pair_list}'
1746
1765
  window.addnstr(y, x + charsWritten, fillStr + trail_str, numChar - charsWritten, boxAttr)
1747
1766
  charsWritten += numChar - charsWritten
@@ -2319,7 +2338,7 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
2319
2338
  if gather_mode: argsList.append('--gather_mode' if not shortend else '-gm')
2320
2339
  if username and username != DEFAULT_USERNAME: argsList.append(f'--username="{username}"' if not shortend else f'-u="{username}"')
2321
2340
  if extraargs and extraargs != DEFAULT_EXTRA_ARGS: argsList.append(f'--extraargs="{extraargs}"' if not shortend else f'-ea="{extraargs}"')
2322
- if skipUnreachable: argsList.append('--skipUnreachable' if not shortend else '-su')
2341
+ if skipUnreachable: argsList.append('--skip_unreachable' if not shortend else '-su')
2323
2342
  if no_env: argsList.append('--no_env')
2324
2343
  if greppable: argsList.append('--greppable' if not shortend else '-g')
2325
2344
  if error_only: argsList.append('--error_only' if not shortend else '-eo')
@@ -2434,9 +2453,12 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2434
2453
  if not max_connections:
2435
2454
  max_connections = 4 * os.cpu_count()
2436
2455
  elif max_connections == 0:
2437
- max_connections = 1048576
2456
+ max_connections = __max_connections_nofile_limit_supported
2438
2457
  elif max_connections < 0:
2439
2458
  max_connections = (-max_connections) * os.cpu_count()
2459
+ if __max_connections_nofile_limit_supported > 0 and max_connections > __max_connections_nofile_limit_supported:
2460
+ eprint(f"Warning: The number of maximum connections {max_connections} is larger than estimated limit {__max_connections_nofile_limit_supported} from ulimit nofile limit {__system_nofile_limit}, setting the maximum connections to {__max_connections_nofile_limit_supported}.")
2461
+ max_connections = __max_connections_nofile_limit_supported
2440
2462
  if not commands:
2441
2463
  commands = []
2442
2464
  else:
@@ -2506,7 +2528,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2506
2528
  command = f"{command}{host}"
2507
2529
  if password and 'sshpass' in _binPaths:
2508
2530
  command = f"{_binPaths['sshpass']} -p {password} {command}"
2509
- hosts.append(Host(host, command,identity_file=identity_file,bash=True,ip = targetHostDic[host]))
2531
+ hosts.append(Host(host, command,identity_file=identity_file,shell=True,ip = targetHostDic[host]))
2510
2532
  else:
2511
2533
  eprint(f"> {command}")
2512
2534
  os.system(command)
@@ -1,7 +0,0 @@
1
- multiSSH3.py,sha256=p7sWcedLIzM45ZRIFFkBo1vOzNlO-9im-He-RsTjb5E,136470
2
- multiSSH3-5.36.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
3
- multiSSH3-5.36.dist-info/METADATA,sha256=YfVb3R1lsmqPW7pIuOXQpkGHSnu6R2WgDhwF_0CyD5E,18160
4
- multiSSH3-5.36.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
5
- multiSSH3-5.36.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
6
- multiSSH3-5.36.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
7
- multiSSH3-5.36.dist-info/RECORD,,