multiSSH3 5.78__py3-none-any.whl → 5.81__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.
multiSSH3.py CHANGED
@@ -55,10 +55,10 @@ except AttributeError:
55
55
  # If neither is available, use a dummy decorator
56
56
  def cache_decorator(func):
57
57
  return func
58
- version = '5.78'
58
+ version = '5.81'
59
59
  VERSION = version
60
60
  __version__ = version
61
- COMMIT_DATE = '2025-06-26'
61
+ COMMIT_DATE = '2025-07-15'
62
62
 
63
63
  CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
64
64
  '~/multiSSH3.config.json',
@@ -934,7 +934,7 @@ def compact_hostnames(Hostnames,verify = True):
934
934
  if not __global_suppress_printout:
935
935
  eprint(f"Error compacting hostnames: {hostSet} -> {compact_hosts}")
936
936
  compact_hosts = hostSet
937
- return compact_hosts
937
+ return sorted(compact_hosts)
938
938
 
939
939
  #%% ------------ Expanding Hostnames ----------------
940
940
  @cache_decorator
@@ -1399,7 +1399,7 @@ def run_command(host, sem, timeout=60,passwds=None, retry_limit = 5):
1399
1399
  else:
1400
1400
  fileArgs = host.files + [f'{host.resolvedName}:{host.command}']
1401
1401
  if useScp:
1402
- formatedCMD = [_binPaths['scp'],'-rpB'] + localExtraArgs + extraargs +['--']+fileArgs
1402
+ formatedCMD = [_binPaths['scp'],'-rp'] + localExtraArgs + extraargs +['--']+fileArgs
1403
1403
  else:
1404
1404
  formatedCMD = [_binPaths['rsync'],'-ahlX','--partial','--inplace', '--info=name'] + rsyncLocalExtraArgs + extraargs +['--']+fileArgs
1405
1405
  else:
@@ -2355,7 +2355,7 @@ def generate_output(hosts, usejson = False, greppable = False):
2355
2355
  outputs.setdefault(hostPrintOut, set()).add(host['name'])
2356
2356
  rtnStr = ''
2357
2357
  for output, hostSet in outputs.items():
2358
- compact_hosts = sorted(compact_hostnames(hostSet))
2358
+ compact_hosts = compact_hostnames(hostSet)
2359
2359
  rtnStr += '*'*80+'\n'
2360
2360
  if __global_suppress_printout:
2361
2361
  rtnStr += f'Abnormal returncode produced by {",".join(compact_hosts)}:\n'
@@ -2443,7 +2443,7 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
2443
2443
  # check for the old content, only update if the new content is different
2444
2444
  if not os.path.exists(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv')):
2445
2445
  with open(os.path.join(tempfile.gettempdir(),f'__{getpass.getuser()}_multiSSH3_UNAVAILABLE_HOSTS.csv'),'w') as f:
2446
- f.writelines(f'{host},{expTime}' for host,expTime in unavailableHosts.values())
2446
+ f.writelines(f'{host},{expTime}' for host,expTime in unavailableHosts.items())
2447
2447
  else:
2448
2448
  oldDic = {}
2449
2449
  try:
@@ -2493,8 +2493,7 @@ def formHostStr(host) -> str:
2493
2493
  if 'local_shell' in host:
2494
2494
  host.remove('local_shell')
2495
2495
  host.add('localhost')
2496
- host = ','.join(host)
2497
- return host
2496
+ return ','.join(compact_hostnames(host))
2498
2497
 
2499
2498
  @cache_decorator
2500
2499
  def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
@@ -2568,7 +2567,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
2568
2567
  history_file = history_file, env_file = env_file,
2569
2568
  repeat = repeat,interval = interval,
2570
2569
  shortend = shortend)
2571
- commands = [command.replace('"', '\\"') for command in commands]
2570
+ commands = [command.replace('"', '\\"').replace('\n', '\\n').replace('\t', '\\t') for command in commands]
2572
2571
  commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
2573
2572
  filePath = os.path.abspath(__file__)
2574
2573
  programName = filePath if filePath else 'mssh'
@@ -2777,7 +2776,7 @@ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAUL
2777
2776
  skipHostsDic = expand_hostnames(skipHostStr.split(','))
2778
2777
  skipHostSet = set(skipHostsDic).union(skipHostsDic.values())
2779
2778
  if skipHostSet:
2780
- eprint(f"Skipping hosts: \"{' '.join(sorted(compact_hostnames(skipHostSet)))}\"")
2779
+ eprint(f"Skipping hosts: \"{' '.join(compact_hostnames(skipHostSet))}\"")
2781
2780
  if copy_id:
2782
2781
  if 'ssh-copy-id' in _binPaths:
2783
2782
  # we will copy the id to the hosts
@@ -3008,23 +3007,9 @@ def write_default_config(args,CONFIG_FILE = None):
3008
3007
  eprint(f'Printing the config file to stdout:')
3009
3008
  print(json.dumps(__configs_from_file, indent=4))
3010
3009
 
3011
- #%% ------------ Wrapper Block ----------------
3012
- def main():
3013
- global _emo
3014
- global __global_suppress_printout
3015
- global __mainReturnCode
3016
- global __failedHosts
3017
- global __ipmiiInterfaceIPPrefix
3010
+ #%% ------------ Argument Processing -----------------
3011
+ def get_parser():
3018
3012
  global _binPaths
3019
- global _env_file
3020
- global __DEBUG_MODE
3021
- global __configs_from_file
3022
- global _encoding
3023
- global __returnZero
3024
- _emo = False
3025
- # We handle the signal
3026
- signal.signal(signal.SIGINT, signal_handler)
3027
- # We parse the arguments
3028
3013
  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 chain: {CONFIG_FILE_CHAIN!r}',
3029
3014
  epilog=f'Found bins: {list(_binPaths.values())}\n Missing bins: {_binCalled - set(_binPaths.keys())}')
3030
3015
  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)
@@ -3075,18 +3060,20 @@ def main():
3075
3060
  parser.add_argument('--script', action='store_true', help='Run the command in script mode, short for -SCRIPT or --no_watch --skip_unreachable --no_env --no_history --greppable --error_only')
3076
3061
  parser.add_argument('-e','--encoding', type=str, help=f'The encoding to use for the output. (default: {DEFAULT_ENCODING})', default=DEFAULT_ENCODING)
3077
3062
  parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} @ {COMMIT_DATE} with [ {", ".join(_binPaths.keys())} ] by {AUTHOR} ({AUTHOR_EMAIL})')
3078
-
3079
- # parser.add_argument('-u', '--user', metavar='user', type=str, nargs=1,
3080
- # help='the user to use to connect to the hosts')
3081
- #args = parser.parse_args()
3063
+ return parser
3082
3064
 
3065
+ def process_args(args = None):
3066
+ parser = get_parser()
3067
+ # We handle the signal
3068
+ signal.signal(signal.SIGINT, signal_handler)
3069
+ # We parse the arguments
3083
3070
  # if python version is 3.7 or higher, use parse_intermixed_args
3084
3071
  try:
3085
- args = parser.parse_intermixed_args()
3072
+ args = parser.parse_intermixed_args(args)
3086
3073
  except Exception :
3087
3074
  #eprint(f"Error while parsing arguments: {e!r}")
3088
3075
  # try to parse the arguments using parse_known_args
3089
- args, unknown = parser.parse_known_args()
3076
+ args, unknown = parser.parse_known_args(args)
3090
3077
  # if there are unknown arguments, we will try to parse them again using parse_args
3091
3078
  if unknown:
3092
3079
  eprint(f"Warning: Unknown arguments, treating all as commands: {unknown!r}")
@@ -3099,10 +3086,14 @@ def main():
3099
3086
  args.no_history = True
3100
3087
  args.greppable = True
3101
3088
  args.error_only = True
3102
-
3103
- if args.return_zero:
3104
- __returnZero = True
3105
3089
 
3090
+ if args.unavailable_host_expiry <= 0:
3091
+ eprint(f"Warning: The unavailable host expiry time {args.unavailable_host_expiry} is less than 0, setting it to 10 seconds.")
3092
+ args.unavailable_host_expiry = 10
3093
+ return args
3094
+
3095
+ def process_config_file(args):
3096
+ global __configs_from_file
3106
3097
  if args.generate_config_file or args.store_config_file:
3107
3098
  if args.store_config_file:
3108
3099
  configFileToWriteTo = args.store_config_file
@@ -3124,11 +3115,12 @@ def main():
3124
3115
  __configs_from_file.update(load_config_file(os.path.expanduser(args.config_file)))
3125
3116
  else:
3126
3117
  eprint(f"Warning: Config file {args.config_file!r} not found, ignoring it.")
3118
+ return args
3127
3119
 
3128
- _env_file = args.env_file
3129
- __DEBUG_MODE = args.debug
3130
3120
  # if there are more than 1 commands, and every command only consists of one word,
3131
3121
  # we will ask the user to confirm if they want to run multiple commands or just one command.
3122
+
3123
+ def process_commands(args):
3132
3124
  if not args.file and len(args.commands) > 1 and all([len(command.split()) == 1 for command in args.commands]):
3133
3125
  eprint(f"Multiple one word command detected, what to do? (1/m/n)")
3134
3126
  eprint(f"1: Run 1 command [{' '.join(args.commands)}] on all hosts ( default )")
@@ -3142,7 +3134,9 @@ def main():
3142
3134
  eprint(f"\nRunning multiple commands: {', '.join(args.commands)!r} on all hosts")
3143
3135
  else:
3144
3136
  _exit_with_code(0, "Aborted by user, no commands to run")
3137
+ return args
3145
3138
 
3139
+ def process_keys(args):
3146
3140
  if args.key or args.use_key:
3147
3141
  if not args.key:
3148
3142
  args.key = find_ssh_key_file()
@@ -3151,23 +3145,43 @@ def main():
3151
3145
  args.key = find_ssh_key_file(args.key)
3152
3146
  elif not os.path.exists(args.key):
3153
3147
  eprint(f"Warning: Identity file {args.key!r} not found. Passing to ssh anyway. Proceed with caution.")
3148
+ return args
3154
3149
 
3150
+
3151
+ def set_global_with_args(args):
3152
+ global _emo
3153
+ global __ipmiiInterfaceIPPrefix
3154
+ global _env_file
3155
+ global __DEBUG_MODE
3156
+ global __configs_from_file
3157
+ global _encoding
3158
+ global __returnZero
3159
+ _emo = False
3155
3160
  __ipmiiInterfaceIPPrefix = args.ipmi_interface_ip_prefix
3161
+ _env_file = args.env_file
3162
+ __DEBUG_MODE = args.debug
3163
+ _encoding = args.encoding
3164
+ if args.return_zero:
3165
+ __returnZero = True
3156
3166
 
3157
- if args.no_output:
3158
- __global_suppress_printout = True
3159
-
3160
- if args.unavailable_host_expiry <= 0:
3161
- eprint(f"Warning: The unavailable host expiry time {args.unavailable_host_expiry} is less than 0, setting it to 10 seconds.")
3162
- args.unavailable_host_expiry = 10
3167
+ #%% ------------ Wrapper Block ----------------
3168
+ def main():
3169
+ global __global_suppress_printout
3170
+ global __mainReturnCode
3171
+ global __failedHosts
3172
+ args = process_args()
3173
+ args = process_config_file(args)
3174
+ args = process_commands(args)
3175
+ args = process_keys(args)
3176
+ set_global_with_args(args)
3163
3177
 
3164
3178
  if args.use_script_timeout:
3165
3179
  # set timeout to the default script timeout if timeout is not set
3166
3180
  if args.timeout == DEFAULT_CLI_TIMEOUT:
3167
3181
  args.timeout = DEFAULT_TIMEOUT
3168
3182
 
3169
- _encoding = args.encoding
3170
-
3183
+ if args.no_output:
3184
+ __global_suppress_printout = True
3171
3185
  if not __global_suppress_printout:
3172
3186
  cmdStr = getStrCommand(args.hosts,args.commands,
3173
3187
  oneonone=args.oneonone,timeout=args.timeout,password=args.password,
@@ -3199,9 +3213,7 @@ def main():
3199
3213
  history_file = args.history_file,
3200
3214
  )
3201
3215
  #print('*'*80)
3202
-
3203
3216
  #if not __global_suppress_printout: eprint('-'*80)
3204
-
3205
3217
  succeededHosts = set()
3206
3218
  for host in hosts:
3207
3219
  if host.returncode and host.returncode != 0:
@@ -3214,12 +3226,12 @@ def main():
3214
3226
  if __mainReturnCode > 0:
3215
3227
  if not __global_suppress_printout:
3216
3228
  eprint(f'Complete. Failed hosts (Return Code not 0) count: {__mainReturnCode}')
3217
- eprint(f'failed_hosts: {",".join(sorted(compact_hostnames(__failedHosts)))}')
3229
+ eprint(f'failed_hosts: {",".join(compact_hostnames(__failedHosts))}')
3218
3230
  else:
3219
3231
  if not __global_suppress_printout: eprint('Complete. All hosts returned 0.')
3220
3232
 
3221
3233
  if args.success_hosts and not __global_suppress_printout:
3222
- eprint(f'succeeded_hosts: {",".join(sorted(compact_hostnames(succeededHosts)))}')
3234
+ eprint(f'succeeded_hosts: {",".join(compact_hostnames(succeededHosts))}')
3223
3235
 
3224
3236
  if threading.active_count() > 1 and not __global_suppress_printout:
3225
3237
  eprint(f'Remaining active thread: {threading.active_count()}')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 5.78
3
+ Version: 5.81
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
@@ -0,0 +1,6 @@
1
+ multiSSH3.py,sha256=oXjDdEpYDWR1dzxzCx8M6H7mQVCQ2xhmlwo7qwm8KeQ,152622
2
+ multissh3-5.81.dist-info/METADATA,sha256=z63eQW7KmmaZrz4-hcFoLmwXlWH69Ai-w7aC2souLms,18093
3
+ multissh3-5.81.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
4
+ multissh3-5.81.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
5
+ multissh3-5.81.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
6
+ multissh3-5.81.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- multiSSH3.py,sha256=SN6BGWEnwE3n_T2GxHkYWYa2JvX1Rru5r6C-gxLINHs,152293
2
- multissh3-5.78.dist-info/METADATA,sha256=nnplqT8neDvB-cdOGgNJMcm-w0II6Qe7D7vRbIM-2aA,18093
3
- multissh3-5.78.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
4
- multissh3-5.78.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
5
- multissh3-5.78.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
6
- multissh3-5.78.dist-info/RECORD,,