multiSSH3 4.76__py3-none-any.whl → 4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: multiSSH3
3
- Version: 4.76
3
+ Version: 4.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
@@ -19,6 +19,45 @@ A script that is able to issue commands to multiple hosts while monitoring their
19
19
  Can be used in bash scripts for automation actions.
20
20
  Also able to be imported and / or use with Flexec SSH Backend to perform cluster automation actions.
21
21
 
22
+ Install via
23
+ ```
24
+ pip install multiSSH3
25
+ ```
26
+
27
+ multiSSH3 will be available as
28
+ ```
29
+ mssh
30
+ mssh3
31
+ multissh
32
+ multissh3
33
+ multiSSH3
34
+ ```
35
+
36
+ multissh will read a config file located at ```/etc/multiSSH3.config.json```
37
+
38
+ To store / generate a config file with the current command line options, you can use
39
+
40
+ ```
41
+ mssh --generate_default_config_file
42
+ ```
43
+
44
+ You can modify the json file directly after generation and multissh will read from it for loading defaults.
45
+
46
+ Note:
47
+
48
+ 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.
49
+
50
+ This option can also be used to store cli options into the config files. For example.
51
+
52
+ ```
53
+ mssh --ipmi_interface_ip_prefix 192 --generate_default_config_file
54
+ ```
55
+ will store
56
+ ```
57
+ "DEFAULT_IPMI_INTERFACE_IP_PREFIX": "192"
58
+ ```
59
+ into the json file.
60
+
22
61
  By defualt reads bash env variables for hostname aliases. Also able to read
23
62
  ```
24
63
  DEFAULT_ENV_FILE = '/etc/profile.d/hosts.sh'
@@ -28,20 +67,20 @@ as hostname aliases.
28
67
  For example:
29
68
  ```
30
69
  export all='192.168.1-2.1-64'
31
- multiSSH3.py all 'echo hi'
70
+ mssh all 'echo hi'
32
71
  ```
33
72
 
34
73
  It is also able to recognize ip blocks / number blocks / hex blocks / character blocks directly.
35
74
 
36
75
  For example:
37
76
  ```
38
- multiSSH3.py testrig[1-10] lsblk
39
- multiSSH3.py ww[a-c],10.100.0.* 'cat /etc/fstab' 'sed -i "/lustre/d' /etc/fstab' 'cat /etc/fstab'
77
+ mssh testrig[1-10] lsblk
78
+ mssh ww[a-c],10.100.0.* 'cat /etc/fstab' 'sed -i "/lustre/d' /etc/fstab' 'cat /etc/fstab'
40
79
  ```
41
80
 
42
81
  It also supports interactive inputs. ( and able to async boardcast to all supplied hosts )
43
82
  ```
44
- multiSSH3.py www bash
83
+ mssh www bash
45
84
  ```
46
85
 
47
86
  By default, it will try to fit everything inside your window.
@@ -52,7 +91,7 @@ DEFAULT_CURSES_MINIMUM_LINE_LEN = 1
52
91
  While leaving minimum 40 characters / 1 line for each host display by default. You can modify this by using -ww and -wh.
53
92
 
54
93
 
55
- Use ```multiSSH3.py --help``` for more info.
94
+ Use ```mssh --help``` for more info.
56
95
 
57
96
  ```
58
97
  usage: mssh [-h] [-u USERNAME] [-ea EXTRAARGS] [-p PASSWORD] [-11] [-f FILE] [--file_sync] [--scp] [-t TIMEOUT] [-r REPEAT] [-i INTERVAL] [--ipmi]
@@ -121,23 +160,26 @@ Following document is generated courtesy of Mr.ChatGPT-o1 Preview:
121
160
 
122
161
  ## Table of Contents
123
162
 
124
- - [Features](#features)
125
- - [Installation](#installation)
126
- - [Usage](#usage)
127
- - [Basic Syntax](#basic-syntax)
128
- - [Command-Line Options](#command-line-options)
129
- - [Examples](#examples)
130
- - [Running a Command on Multiple Hosts](#running-a-command-on-multiple-hosts)
131
- - [Copying Files to Multiple Hosts](#copying-files-to-multiple-hosts)
132
- - [Using Hostname Ranges](#using-hostname-ranges)
133
- - [Using IPMI](#using-ipmi)
134
- - [Using Password Authentication](#using-password-authentication)
135
- - [Skipping Unreachable Hosts](#skipping-unreachable-hosts)
136
- - [JSON Output](#json-output)
137
- - [Quiet Mode](#quiet-mode)
138
- - [Environment Variables](#environment-variables)
139
- - [Notes](#notes)
140
- - [License](#license)
163
+ - [multiSSH3](#multissh3)
164
+ - [multissh](#multissh)
165
+ - [Table of Contents](#table-of-contents)
166
+ - [Features](#features)
167
+ - [Installation](#installation)
168
+ - [Usage](#usage)
169
+ - [Basic Syntax](#basic-syntax)
170
+ - [Command-Line Options](#command-line-options)
171
+ - [Examples](#examples)
172
+ - [Running a Command on Multiple Hosts](#running-a-command-on-multiple-hosts)
173
+ - [Copying Files to Multiple Hosts](#copying-files-to-multiple-hosts)
174
+ - [Using Hostname Ranges](#using-hostname-ranges)
175
+ - [Using IPMI](#using-ipmi)
176
+ - [Using Password Authentication](#using-password-authentication)
177
+ - [Skipping Unreachable Hosts](#skipping-unreachable-hosts)
178
+ - [JSON Output](#json-output)
179
+ - [Quiet Mode](#quiet-mode)
180
+ - [Environment Variables](#environment-variables)
181
+ - [Notes](#notes)
182
+ - [License](#license)
141
183
 
142
184
  ## Features
143
185
 
@@ -0,0 +1,7 @@
1
+ multiSSH3.py,sha256=Nerzo1GexP3GoZv0uhjydtu2zSpw4vY2k4Rx0F3yD8M,81707
2
+ multiSSH3-4.81.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
3
+ multiSSH3-4.81.dist-info/METADATA,sha256=GnSUBBxKq9cvqK4Z2cMVsOqWMgjqL_vpEO5XOZ0mNV8,16515
4
+ multiSSH3-4.81.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
5
+ multiSSH3-4.81.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
6
+ multiSSH3-4.81.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
7
+ multiSSH3-4.81.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (74.1.2)
2
+ Generator: setuptools (75.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
multiSSH3.py CHANGED
@@ -15,6 +15,7 @@ import io
15
15
  import signal
16
16
  import functools
17
17
  import glob
18
+ #import fnmatch
18
19
  try:
19
20
  # Check if functiools.cache is available
20
21
  cache_decorator = functools.cache
@@ -26,47 +27,130 @@ except AttributeError:
26
27
  # If neither is available, use a dummy decorator
27
28
  def cache_decorator(func):
28
29
  return func
29
-
30
- version = '4.76'
30
+ version = '4.81'
31
31
  VERSION = version
32
32
 
33
- DEFAULT_ENV_FILE = '/etc/profile.d/hosts.sh'
34
- DEFAULT_USERNAME = None
35
- DEFAULT_EXTRA_ARGS = None
36
- DEFAULT_PASSWORD = ''
37
- DEFAULT_ONE_ON_ONE = False
38
- DEFAULT_FILE_SYNC = False
39
- DEFAULT_SCP = False
40
- DEFAULT_TIMEOUT = 50
41
- DEFAULT_REPEAT = 1
42
- DEFAULT_INTERVAL = 0
43
- DEFAULT_IPMI = False
44
- DEFAULT_INTERFACE_IP_PREFIX = None
45
- DEFAULT_IPMI_INTERFACE_IP_PREFIX = None
46
- DEFAULT_QUIET = False
47
- DEFAULT_ERROR_ONLY = False
48
- DEFAULT_NO_OUTPUT = False
49
- DEFAULT_NO_ENV = False
50
- DEFAULT_MAX_CONNECTIONS = 4 * os.cpu_count()
51
- DEFAULT_JSON_MODE = False
52
- DEFAULT_PRINT_SUCCESS_HOSTS = False
53
- DEFAULT_GREPPABLE_MODE = False
54
- DEFAULT_NO_WATCH = False
55
- DEFAULT_SKIP_UNREACHABLE = False
56
- DEFAULT_SKIP_HOSTS = ''
57
- DEFAULT_CURSES_MINIMUM_CHAR_LEN = 40
58
- DEFAULT_CURSES_MINIMUM_LINE_LEN = 1
59
- DEFAULT_SINGLE_WINDOW = False
60
-
61
- DEFAULT_CALLED = True
62
- DEFAULT_RETURN_UNFINISHED = False
63
- DEFAULT_UPDATE_UNREACHABLE_HOSTS = True
64
- DEFAULT_NO_START = False
65
-
66
- global_suppress_printout = True
67
-
68
- mainReturnCode = 0
69
- failedHosts = set()
33
+ CONFIG_FILE = '/etc/multiSSH3.config.json'
34
+
35
+ def load_config_file(config_file):
36
+ '''
37
+ Load the config file to global variables
38
+
39
+ Args:
40
+ config_file (str): The config file
41
+
42
+ Returns:
43
+ dict: The config
44
+ '''
45
+ if not os.path.exists(config_file):
46
+ return {}
47
+ with open(config_file,'r') as f:
48
+ config = json.load(f)
49
+ return config
50
+
51
+ __configs_from_file = load_config_file(CONFIG_FILE)
52
+
53
+ __build_in_default_config = {
54
+ 'AUTHOR': 'Yufei Pan',
55
+ 'AUTHOR_EMAIL': 'pan@zopyr.us',
56
+ 'DEFAULT_HOSTS': 'all',
57
+ 'DEFAULT_USERNAME': None,
58
+ 'DEFAULT_PASSWORD': '',
59
+ 'DEFAULT_EXTRA_ARGS': None,
60
+ 'DEFAULT_ONE_ON_ONE': False,
61
+ 'DEFAULT_SCP': False,
62
+ 'DEFAULT_FILE_SYNC': False,
63
+ 'DEFAULT_TIMEOUT': 50,
64
+ 'DEFAULT_CLI_TIMEOUT': 0,
65
+ 'DEFAULT_REPEAT': 1,
66
+ 'DEFAULT_INTERVAL': 0,
67
+ 'DEFAULT_IPMI': False,
68
+ 'DEFAULT_IPMI_INTERFACE_IP_PREFIX': '',
69
+ 'DEFAULT_INTERFACE_IP_PREFIX': None,
70
+ 'DEFAULT_NO_WATCH': False,
71
+ 'DEFAULT_CURSES_MINIMUM_CHAR_LEN': 40,
72
+ 'DEFAULT_CURSES_MINIMUM_LINE_LEN': 1,
73
+ 'DEFAULT_SINGLE_WINDOW': False,
74
+ 'DEFAULT_ERROR_ONLY': False,
75
+ 'DEFAULT_NO_OUTPUT': False,
76
+ 'DEFAULT_NO_ENV': False,
77
+ 'DEFAULT_ENV_FILE': '/etc/profile.d/hosts.sh',
78
+ 'DEFAULT_MAX_CONNECTIONS': 4 * os.cpu_count(),
79
+ 'DEFAULT_JSON_MODE': False,
80
+ 'DEFAULT_PRINT_SUCCESS_HOSTS': False,
81
+ 'DEFAULT_GREPPABLE_MODE': False,
82
+ 'DEFAULT_SKIP_UNREACHABLE': False,
83
+ 'DEFAULT_SKIP_HOSTS': '',
84
+ 'ERROR_MESSAGES_TO_IGNORE': [
85
+ 'Pseudo-terminal will not be allocated because stdin is not a terminal',
86
+ 'Connection to .* closed',
87
+ 'Warning: Permanently added',
88
+ 'mux_client_request_session',
89
+ 'disabling multiplexing',
90
+ ],
91
+ '_DEFAULT_CALLED': True,
92
+ '_DEFAULT_RETURN_UNFINISHED': False,
93
+ '_DEFAULT_UPDATE_UNREACHABLE_HOSTS': True,
94
+ '_DEFAULT_NO_START': False,
95
+ '_etc_hosts': {},
96
+ '_sshpassAvailable': False,
97
+ '__ERROR_MESSAGES_TO_IGNORE_REGEX':None,
98
+ }
99
+
100
+ AUTHOR = __configs_from_file.get('AUTHOR', __build_in_default_config['AUTHOR'])
101
+ AUTHOR_EMAIL = __configs_from_file.get('AUTHOR_EMAIL', __build_in_default_config['AUTHOR_EMAIL'])
102
+
103
+ DEFAULT_HOSTS = __configs_from_file.get('DEFAULT_HOSTS', __build_in_default_config['DEFAULT_HOSTS'])
104
+ DEFAULT_ENV_FILE = __configs_from_file.get('DEFAULT_ENV_FILE', __build_in_default_config['DEFAULT_ENV_FILE'])
105
+ DEFAULT_USERNAME = __configs_from_file.get('DEFAULT_USERNAME', __build_in_default_config['DEFAULT_USERNAME'])
106
+ DEFAULT_PASSWORD = __configs_from_file.get('DEFAULT_PASSWORD', __build_in_default_config['DEFAULT_PASSWORD'])
107
+ DEFAULT_EXTRA_ARGS = __configs_from_file.get('DEFAULT_EXTRA_ARGS', __build_in_default_config['DEFAULT_EXTRA_ARGS'])
108
+ DEFAULT_ONE_ON_ONE = __configs_from_file.get('DEFAULT_ONE_ON_ONE', __build_in_default_config['DEFAULT_ONE_ON_ONE'])
109
+ DEFAULT_SCP = __configs_from_file.get('DEFAULT_SCP', __build_in_default_config['DEFAULT_SCP'])
110
+ DEFAULT_FILE_SYNC = __configs_from_file.get('DEFAULT_FILE_SYNC', __build_in_default_config['DEFAULT_FILE_SYNC'])
111
+ DEFAULT_TIMEOUT = __configs_from_file.get('DEFAULT_TIMEOUT', __build_in_default_config['DEFAULT_TIMEOUT'])
112
+ DEFAULT_CLI_TIMEOUT = __configs_from_file.get('DEFAULT_CLI_TIMEOUT', __build_in_default_config['DEFAULT_CLI_TIMEOUT'])
113
+ DEFAULT_REPEAT = __configs_from_file.get('DEFAULT_REPEAT', __build_in_default_config['DEFAULT_REPEAT'])
114
+ DEFAULT_INTERVAL = __configs_from_file.get('DEFAULT_INTERVAL', __build_in_default_config['DEFAULT_INTERVAL'])
115
+ DEFAULT_IPMI = __configs_from_file.get('DEFAULT_IPMI', __build_in_default_config['DEFAULT_IPMI'])
116
+ DEFAULT_IPMI_INTERFACE_IP_PREFIX = __configs_from_file.get('DEFAULT_IPMI_INTERFACE_IP_PREFIX', __build_in_default_config['DEFAULT_IPMI_INTERFACE_IP_PREFIX'])
117
+ DEFAULT_INTERFACE_IP_PREFIX = __configs_from_file.get('DEFAULT_INTERFACE_IP_PREFIX', __build_in_default_config['DEFAULT_INTERFACE_IP_PREFIX'])
118
+ DEFAULT_NO_WATCH = __configs_from_file.get('DEFAULT_NO_WATCH', __build_in_default_config['DEFAULT_NO_WATCH'])
119
+ DEFAULT_CURSES_MINIMUM_CHAR_LEN = __configs_from_file.get('DEFAULT_CURSES_MINIMUM_CHAR_LEN', __build_in_default_config['DEFAULT_CURSES_MINIMUM_CHAR_LEN'])
120
+ DEFAULT_CURSES_MINIMUM_LINE_LEN = __configs_from_file.get('DEFAULT_CURSES_MINIMUM_LINE_LEN', __build_in_default_config['DEFAULT_CURSES_MINIMUM_LINE_LEN'])
121
+ DEFAULT_SINGLE_WINDOW = __configs_from_file.get('DEFAULT_SINGLE_WINDOW', __build_in_default_config['DEFAULT_SINGLE_WINDOW'])
122
+ DEFAULT_ERROR_ONLY = __configs_from_file.get('DEFAULT_ERROR_ONLY', __build_in_default_config['DEFAULT_ERROR_ONLY'])
123
+ DEFAULT_NO_OUTPUT = __configs_from_file.get('DEFAULT_NO_OUTPUT', __build_in_default_config['DEFAULT_NO_OUTPUT'])
124
+ DEFAULT_NO_ENV = __configs_from_file.get('DEFAULT_NO_ENV', __build_in_default_config['DEFAULT_NO_ENV'])
125
+ DEFAULT_MAX_CONNECTIONS = __configs_from_file.get('DEFAULT_MAX_CONNECTIONS', __build_in_default_config['DEFAULT_MAX_CONNECTIONS'])
126
+ if not DEFAULT_MAX_CONNECTIONS:
127
+ DEFAULT_MAX_CONNECTIONS = 4 * os.cpu_count()
128
+ DEFAULT_JSON_MODE = __configs_from_file.get('DEFAULT_JSON_MODE', __build_in_default_config['DEFAULT_JSON_MODE'])
129
+ DEFAULT_PRINT_SUCCESS_HOSTS = __configs_from_file.get('DEFAULT_PRINT_SUCCESS_HOSTS', __build_in_default_config['DEFAULT_PRINT_SUCCESS_HOSTS'])
130
+ DEFAULT_GREPPABLE_MODE = __configs_from_file.get('DEFAULT_GREPPABLE_MODE', __build_in_default_config['DEFAULT_GREPPABLE_MODE'])
131
+ DEFAULT_SKIP_UNREACHABLE = __configs_from_file.get('DEFAULT_SKIP_UNREACHABLE', __build_in_default_config['DEFAULT_SKIP_UNREACHABLE'])
132
+ DEFAULT_SKIP_HOSTS = __configs_from_file.get('DEFAULT_SKIP_HOSTS', __build_in_default_config['DEFAULT_SKIP_HOSTS'])
133
+
134
+ ERROR_MESSAGES_TO_IGNORE = __configs_from_file.get('ERROR_MESSAGES_TO_IGNORE', __build_in_default_config['ERROR_MESSAGES_TO_IGNORE'])
135
+
136
+ _DEFAULT_CALLED = __configs_from_file.get('_DEFAULT_CALLED', __build_in_default_config['_DEFAULT_CALLED'])
137
+ _DEFAULT_RETURN_UNFINISHED = __configs_from_file.get('_DEFAULT_RETURN_UNFINISHED', __build_in_default_config['_DEFAULT_RETURN_UNFINISHED'])
138
+ _DEFAULT_UPDATE_UNREACHABLE_HOSTS = __configs_from_file.get('_DEFAULT_UPDATE_UNREACHABLE_HOSTS', __build_in_default_config['_DEFAULT_UPDATE_UNREACHABLE_HOSTS'])
139
+ _DEFAULT_NO_START = __configs_from_file.get('_DEFAULT_NO_START', __build_in_default_config['_DEFAULT_NO_START'])
140
+
141
+ # form the regex from the list
142
+ if '__ERROR_MESSAGES_TO_IGNORE_REGEX' in __configs_from_file:
143
+ print('Using __ERROR_MESSAGES_TO_IGNORE_REGEX from config file, ignoring ERROR_MESSAGES_TO_IGNORE')
144
+ __ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile(__configs_from_file['__ERROR_MESSAGES_TO_IGNORE_REGEX'])
145
+ else:
146
+ __ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile('|'.join(ERROR_MESSAGES_TO_IGNORE))
147
+
148
+
149
+
150
+ __global_suppress_printout = True
151
+
152
+ __mainReturnCode = 0
153
+ __failedHosts = set()
70
154
  class Host:
71
155
  def __init__(self, name, command, files = None,ipmi = False,interface_ip_prefix = None,scp=False,extraargs=None):
72
156
  self.name = name # the name of the host (hostname or IP address)
@@ -90,25 +174,25 @@ class Host:
90
174
  def __str__(self):
91
175
  return f"Host(name={self.name}, command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr})"
92
176
 
93
- wildCharacters = ['*','?','x']
177
+ __wildCharacters = ['*','?','x']
94
178
 
95
- gloablUnavailableHosts = set()
179
+ __gloablUnavailableHosts = set()
96
180
 
97
- ipmiiInterfaceIPPrefix = DEFAULT_IPMI_INTERFACE_IP_PREFIX
181
+ __ipmiiInterfaceIPPrefix = DEFAULT_IPMI_INTERFACE_IP_PREFIX
98
182
 
99
- keyPressesIn = [[]]
183
+ __keyPressesIn = [[]]
100
184
 
101
- emo = False
185
+ _emo = False
102
186
 
103
- etc_hosts = {}
187
+ _etc_hosts = __configs_from_file.get('_etc_hosts', __build_in_default_config['_etc_hosts'])
104
188
 
105
- env_file = DEFAULT_ENV_FILE
189
+ _env_file = DEFAULT_ENV_FILE
106
190
 
107
191
  # check if command sshpass is available
108
- sshpassAvailable = False
192
+ _sshpassAvailable = __configs_from_file.get('_sshpassAvailable', __build_in_default_config['_sshpassAvailable'])
109
193
  try:
110
194
  subprocess.run(['which', 'sshpass'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
111
- sshpassAvailable = True
195
+ _sshpassAvailable = True
112
196
  except:
113
197
  pass
114
198
 
@@ -135,14 +219,14 @@ def expandIPv4Address(hosts):
135
219
  # Handle wildcards
136
220
  octetRange = octet.split('-')
137
221
  for i in range(len(octetRange)):
138
- if not octetRange[i] or octetRange[i] in wildCharacters:
222
+ if not octetRange[i] or octetRange[i] in __wildCharacters:
139
223
  if i == 0:
140
224
  octetRange[i] = '0'
141
225
  elif i == 1:
142
226
  octetRange[i] = '255'
143
227
 
144
228
  expandedOctets.append([str(i) for i in range(int(octetRange[0]),int(octetRange[1])+1)])
145
- elif octet in wildCharacters:
229
+ elif octet in __wildCharacters:
146
230
  expandedOctets.append([str(i) for i in range(0,256)])
147
231
  else:
148
232
  expandedOctets.append([octet])
@@ -171,11 +255,11 @@ def readEnvFromFile(environemnt_file = ''):
171
255
  return env
172
256
  except:
173
257
  env = {}
174
- global env_file
258
+ global _env_file
175
259
  if environemnt_file:
176
260
  envf = environemnt_file
177
261
  else:
178
- envf = env_file if env_file else DEFAULT_ENV_FILE
262
+ envf = _env_file if _env_file else DEFAULT_ENV_FILE
179
263
  if os.path.exists(envf):
180
264
  with open(envf,'r') as f:
181
265
  for line in f:
@@ -200,7 +284,7 @@ def getIP(hostname,local=False):
200
284
  Returns:
201
285
  str: The IP address of the hostname
202
286
  '''
203
- global etc_hosts
287
+ global _etc_hosts
204
288
  # First we check if the hostname is an IP address
205
289
  try:
206
290
  ipaddress.ip_address(hostname)
@@ -208,7 +292,7 @@ def getIP(hostname,local=False):
208
292
  except ValueError:
209
293
  pass
210
294
  # Then we check /etc/hosts
211
- if not etc_hosts and os.path.exists('/etc/hosts'):
295
+ if not _etc_hosts and os.path.exists('/etc/hosts'):
212
296
  with open('/etc/hosts','r') as f:
213
297
  for line in f:
214
298
  if line.startswith('#') or not line.strip():
@@ -219,9 +303,9 @@ def getIP(hostname,local=False):
219
303
  continue
220
304
  ip = chunks[0]
221
305
  for host in chunks[1:]:
222
- etc_hosts[host] = ip
223
- if hostname in etc_hosts:
224
- return etc_hosts[hostname]
306
+ _etc_hosts[host] = ip
307
+ if hostname in _etc_hosts:
308
+ return _etc_hosts[hostname]
225
309
  if local:
226
310
  return None
227
311
  # Then we check the DNS
@@ -375,10 +459,10 @@ def validate_expand_hostname(hostname,no_env=False):
375
459
  return [hostname]
376
460
  else:
377
461
  print(f"Error: {hostname} is not a valid hostname or IP address!")
378
- global mainReturnCode
379
- mainReturnCode += 1
380
- global failedHosts
381
- failedHosts.add(hostname)
462
+ global __mainReturnCode
463
+ __mainReturnCode += 1
464
+ global __failedHosts
465
+ __failedHosts.add(hostname)
382
466
  return []
383
467
 
384
468
  def input_with_timeout_and_countdown(timeout, prompt='Please enter your selection'):
@@ -458,28 +542,28 @@ def handle_writing_stream(stream,stop_event,host):
458
542
  Returns:
459
543
  None
460
544
  '''
461
- global keyPressesIn
462
- # keyPressesIn is a list of lists.
545
+ global __keyPressesIn
546
+ # __keyPressesIn is a list of lists.
463
547
  # Each list is a list of characters to be sent to the stdin of the process at once.
464
548
  # We do not send the last line as it may be incomplete.
465
549
  sentInput = 0
466
550
  while not stop_event.is_set():
467
- if sentInput < len(keyPressesIn) - 1 :
468
- stream.write(''.join(keyPressesIn[sentInput]).encode())
551
+ if sentInput < len(__keyPressesIn) - 1 :
552
+ stream.write(''.join(__keyPressesIn[sentInput]).encode())
469
553
  stream.flush()
470
- host.output.append(' $ ' + ''.join(keyPressesIn[sentInput]).encode().decode().replace('\n', '↵'))
471
- host.stdout.append(' $ ' + ''.join(keyPressesIn[sentInput]).encode().decode().replace('\n', '↵'))
554
+ host.output.append(' $ ' + ''.join(__keyPressesIn[sentInput]).encode().decode().replace('\n', '↵'))
555
+ host.stdout.append(' $ ' + ''.join(__keyPressesIn[sentInput]).encode().decode().replace('\n', '↵'))
472
556
  sentInput += 1
473
557
  else:
474
558
  time.sleep(0.1)
475
- if sentInput < len(keyPressesIn) - 1 :
476
- print(f"Warning: {len(keyPressesIn)-sentInput} key presses are not sent before the process is terminated!")
559
+ if sentInput < len(__keyPressesIn) - 1 :
560
+ print(f"Warning: {len(__keyPressesIn)-sentInput} key presses are not sent before the process is terminated!")
477
561
  # # send the last line
478
- # if keyPressesIn and keyPressesIn[-1]:
479
- # stream.write(''.join(keyPressesIn[-1]).encode())
562
+ # if __keyPressesIn and __keyPressesIn[-1]:
563
+ # stream.write(''.join(__keyPressesIn[-1]).encode())
480
564
  # stream.flush()
481
- # host.output.append(' $ ' + ''.join(keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
482
- # host.stdout.append(' $ ' + ''.join(keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
565
+ # host.output.append(' $ ' + ''.join(__keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
566
+ # host.stdout.append(' $ ' + ''.join(__keyPressesIn[-1]).encode().decode().replace('\n', '↵'))
483
567
  return sentInput
484
568
 
485
569
  def ssh_command(host, sem, timeout=60,passwds=None):
@@ -495,7 +579,10 @@ def ssh_command(host, sem, timeout=60,passwds=None):
495
579
  Returns:
496
580
  None
497
581
  '''
498
- global emo
582
+ global _emo
583
+ global __ERROR_MESSAGES_TO_IGNORE_REGEX
584
+ global __ipmiiInterfaceIPPrefix
585
+ global _sshpassAvailable
499
586
  with sem:
500
587
  try:
501
588
  host.username = None
@@ -514,8 +601,8 @@ def ssh_command(host, sem, timeout=60,passwds=None):
514
601
  extraargs = host.extraargs.split()
515
602
  else:
516
603
  extraargs = []
517
- if ipmiiInterfaceIPPrefix:
518
- host.interface_ip_prefix = ipmiiInterfaceIPPrefix if host.ipmi and not host.interface_ip_prefix else host.interface_ip_prefix
604
+ if __ipmiiInterfaceIPPrefix:
605
+ host.interface_ip_prefix = __ipmiiInterfaceIPPrefix if host.ipmi and not host.interface_ip_prefix else host.interface_ip_prefix
519
606
  if host.interface_ip_prefix:
520
607
  try:
521
608
  hostOctets = getIP(host.address,local=False).split('.')
@@ -538,21 +625,21 @@ def ssh_command(host, sem, timeout=60,passwds=None):
538
625
  formatedCMD = ['bash','-c',f'ipmitool -H {host.address} -U {host.username} {" ".join(extraargs)} {host.command}']
539
626
  else:
540
627
  if host.files:
541
- if host.scp:
542
- formatedCMD = ['scp','-rpB'] + extraargs +['--']+host.files+[f'{host.resolvedName}:{host.command}']
543
- else:
544
- formatedCMD = ['rsync','-ahlX','--partial','--inplace', '--info=name'] + extraargs +['--']+host.files+[f'{host.resolvedName}:{host.command}']
628
+ if host.scp:
629
+ formatedCMD = ['scp','-rpB'] + extraargs +['--']+host.files+[f'{host.resolvedName}:{host.command}']
630
+ else:
631
+ formatedCMD = ['rsync','-ahlX','--partial','--inplace', '--info=name'] + extraargs +['--']+host.files+[f'{host.resolvedName}:{host.command}']
545
632
  else:
546
633
  formatedCMD = ['ssh'] + extraargs +['--']+ [host.resolvedName, host.command]
547
- if passwds and sshpassAvailable:
634
+ if passwds and _sshpassAvailable:
548
635
  formatedCMD = ['sshpass', '-p', passwds] + formatedCMD
549
636
  elif passwds:
550
637
  host.output.append('Warning: sshpass is not available. Please install sshpass to use password authentication.')
551
638
  #host.stderr.append('Warning: sshpass is not available. Please install sshpass to use password authentication.')
552
639
  host.output.append('Please provide password via live input or use ssh key authentication.')
553
- # # try to send the password via keyPressesIn
554
- # keyPressesIn[-1] = list(passwds) + ['\n']
555
- # keyPressesIn.append([])
640
+ # # try to send the password via __keyPressesIn
641
+ # __keyPressesIn[-1] = list(passwds) + ['\n']
642
+ # __keyPressesIn.append([])
556
643
  host.output.append('Running command: '+' '.join(formatedCMD))
557
644
  #host.stdout = []
558
645
  proc = subprocess.Popen(formatedCMD,stdout=subprocess.PIPE,stderr=subprocess.PIPE,stdin=subprocess.PIPE)
@@ -592,7 +679,7 @@ def ssh_command(host, sem, timeout=60,passwds=None):
592
679
  host.printedLines -= 1
593
680
  host.output.append(timeoutLine)
594
681
  outLength = len(host.output)
595
- if emo:
682
+ if _emo:
596
683
  host.stderr.append('Ctrl C detected, Emergency Stop!')
597
684
  host.output.append('Ctrl C detected, Emergency Stop!')
598
685
  proc.send_signal(signal.SIGINT)
@@ -607,7 +694,7 @@ def ssh_command(host, sem, timeout=60,passwds=None):
607
694
  stdin_thread.join(timeout=1)
608
695
  # here we handle the rest of the stdout after the subprocess returns
609
696
  host.output.append(f'Pipe Closed. Trying to read the rest of the stdout...')
610
- if not emo:
697
+ if not _emo:
611
698
  stdout = None
612
699
  stderr = None
613
700
  try:
@@ -627,8 +714,9 @@ def ssh_command(host, sem, timeout=60,passwds=None):
627
714
  elif host.stderr and host.stderr[-1].strip().startswith('Ctrl C detected, Emergency Stop!'):
628
715
  host.returncode = 137
629
716
  host.output.append(f'Command finished with return code {host.returncode}')
630
- if host.stderr and host.stderr[-1].strip().startswith('Connection to ') and host.stderr[-1].strip().endswith(' closed.'):
631
- host.stderr.pop()
717
+ if host.stderr:
718
+ # filter out the error messages that we want to ignore
719
+ host.stderr = [line for line in host.stderr if not __ERROR_MESSAGES_TO_IGNORE_REGEX.search(line)]
632
720
  except Exception as e:
633
721
  import traceback
634
722
  host.stderr.extend(str(e).split('\n'))
@@ -764,7 +852,7 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
764
852
  last_refresh_time = time.perf_counter()
765
853
  stdscr.clear()
766
854
  #host_window.refresh()
767
- global keyPressesIn
855
+ global __keyPressesIn
768
856
  stdscr.nodelay(True)
769
857
  # we generate a stats window at the top of the screen
770
858
  stat_window = curses.newwin(1, max_x, 0, 0)
@@ -803,19 +891,19 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
803
891
  elif key in [259, 258, 260, 261, 339, 338, 262, 360]:
804
892
  # if the key is up arrow, we will move the line to display up
805
893
  if key == 259: # 259 is the key code for up arrow
806
- lineToDisplay = max(lineToDisplay - 1, -len(keyPressesIn))
894
+ lineToDisplay = max(lineToDisplay - 1, -len(__keyPressesIn))
807
895
  # if the key is down arrow, we will move the line to display down
808
896
  elif key == 258: # 258 is the key code for down arrow
809
897
  lineToDisplay = min(lineToDisplay + 1, -1)
810
898
  # if the key is left arrow, we will move the cursor left
811
899
  elif key == 260: # 260 is the key code for left arrow
812
- curserPosition = min(max(curserPosition - 1, 0), len(keyPressesIn[lineToDisplay]) -1)
900
+ curserPosition = min(max(curserPosition - 1, 0), len(__keyPressesIn[lineToDisplay]) -1)
813
901
  # if the key is right arrow, we will move the cursor right
814
902
  elif key == 261: # 261 is the key code for right arrow
815
- curserPosition = max(min(curserPosition + 1, len(keyPressesIn[lineToDisplay])), 0)
903
+ curserPosition = max(min(curserPosition + 1, len(__keyPressesIn[lineToDisplay])), 0)
816
904
  # if the key is page up, we will move the line to display up by 5 lines
817
905
  elif key == 339: # 339 is the key code for page up
818
- lineToDisplay = max(lineToDisplay - 5, -len(keyPressesIn))
906
+ lineToDisplay = max(lineToDisplay - 5, -len(__keyPressesIn))
819
907
  # if the key is page down, we will move the line to display down by 5 lines
820
908
  elif key == 338: # 338 is the key code for page down
821
909
  lineToDisplay = min(lineToDisplay + 5, -1)
@@ -824,48 +912,48 @@ def generate_display(stdscr, hosts, threads,lineToDisplay = -1,curserPosition =
824
912
  curserPosition = 0
825
913
  # if the key is end, we will move the cursor to the end of the line
826
914
  elif key == 360: # 360 is the key code for end
827
- curserPosition = len(keyPressesIn[lineToDisplay])
915
+ curserPosition = len(__keyPressesIn[lineToDisplay])
828
916
  # We are left with these are keys that mofidy the current line.
829
917
  else:
830
918
  # This means the user have done scrolling and is committing to modify the current line.
831
919
  if lineToDisplay < -1:
832
920
  # We overwrite the last line (current working line) with the line to display, removing the newline at the end
833
- keyPressesIn[-1] = keyPressesIn[lineToDisplay][:-1]
921
+ __keyPressesIn[-1] = __keyPressesIn[lineToDisplay][:-1]
834
922
  lineToDisplay = -1
835
- curserPosition = max(0, min(curserPosition, len(keyPressesIn[lineToDisplay])))
923
+ curserPosition = max(0, min(curserPosition, len(__keyPressesIn[lineToDisplay])))
836
924
  if key == 10: # 10 is the key code for newline
837
- keyPressesIn[-1].append(chr(key))
838
- keyPressesIn.append([])
925
+ __keyPressesIn[-1].append(chr(key))
926
+ __keyPressesIn.append([])
839
927
  lineToDisplay = -1
840
928
  curserPosition = 0
841
929
  # if the key is backspace, we will remove the last character from the last list
842
930
  elif key in [8,263]: # 8 is the key code for backspace
843
931
  if curserPosition > 0:
844
- keyPressesIn[lineToDisplay].pop(curserPosition - 1)
932
+ __keyPressesIn[lineToDisplay].pop(curserPosition - 1)
845
933
  curserPosition -= 1
846
934
  # if the key is ESC, we will clear the last list
847
935
  elif key == 27: # 27 is the key code for ESC
848
- keyPressesIn[-1] = []
936
+ __keyPressesIn[-1] = []
849
937
  curserPosition = 0
850
938
  # ignore delete key
851
939
  elif key in [127, 330]: # 330 is the key code for delete key
852
940
  # delete the character at the cursor position
853
- if curserPosition < len(keyPressesIn[lineToDisplay]):
854
- keyPressesIn[lineToDisplay].pop(curserPosition)
941
+ if curserPosition < len(__keyPressesIn[lineToDisplay]):
942
+ __keyPressesIn[lineToDisplay].pop(curserPosition)
855
943
  else:
856
944
  # if the key is not a special key, we will add it
857
- keyPressesIn[lineToDisplay].insert(curserPosition, chr(key))
945
+ __keyPressesIn[lineToDisplay].insert(curserPosition, chr(key))
858
946
  curserPosition += 1
859
947
  # reconfigure when the terminal size changes
860
948
  # raise Exception when max_y or max_x is changed, let parent handle reconfigure
861
949
  if org_dim != stdscr.getmaxyx():
862
950
  raise Exception('Terminal size changed. Please reconfigure window.')
863
951
  # We generate the aggregated stats if user did not input anything
864
- if not keyPressesIn[lineToDisplay]:
952
+ if not __keyPressesIn[lineToDisplay]:
865
953
  stats = '┍'+ f"Total: {len(hosts)} Running: {host_stats['running']} Failed: {host_stats['failed']} Finished: {host_stats['finished']} Waiting: {host_stats['waiting']}"[:max_x - 2].center(max_x - 2, "━")
866
954
  else:
867
955
  # we use the stat bar to display the key presses
868
- encodedLine = ''.join(keyPressesIn[lineToDisplay]).encode().decode().strip('\n') + ' '
956
+ encodedLine = ''.join(__keyPressesIn[lineToDisplay]).encode().decode().strip('\n') + ' '
869
957
  # # add the flashing indicator at the curse position
870
958
  # if time.perf_counter() % 1 > 0.5:
871
959
  # encodedLine = encodedLine[:curserPosition] + '█' + encodedLine[curserPosition:]
@@ -985,8 +1073,8 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
985
1073
  Returns:
986
1074
  str: The pretty output generated
987
1075
  '''
988
- global keyPressesIn
989
- global global_suppress_printout
1076
+ global __keyPressesIn
1077
+ global __global_suppress_printout
990
1078
  hosts = [dict(host) for host in hosts]
991
1079
  if usejson:
992
1080
  # [print(dict(host)) for host in hosts]
@@ -1009,14 +1097,14 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
1009
1097
  rtnStr = ''
1010
1098
  for output, hosts in outputs.items():
1011
1099
  rtnStr += f"{','.join(hosts)}{output}\n"
1012
- if keyPressesIn[-1]:
1013
- CMDsOut = [''.join(cmd).encode('unicode_escape').decode().replace('\\n', '↵') for cmd in keyPressesIn if cmd]
1100
+ if __keyPressesIn[-1]:
1101
+ CMDsOut = [''.join(cmd).encode('unicode_escape').decode().replace('\\n', '↵') for cmd in __keyPressesIn if cmd]
1014
1102
  rtnStr += 'User Inputs: '+ '\nUser Inputs: '.join(CMDsOut)
1015
1103
  #rtnStr += '\n'
1016
1104
  else:
1017
1105
  outputs = {}
1018
1106
  for host in hosts:
1019
- if global_suppress_printout:
1107
+ if __global_suppress_printout:
1020
1108
  if host['returncode'] == 0:
1021
1109
  continue
1022
1110
  hostPrintOut = f" Command:\n {host['command']}\n"
@@ -1032,24 +1120,24 @@ def print_output(hosts,usejson = False,quiet = False,greppable = False):
1032
1120
  outputs[hostPrintOut].append(host['name'])
1033
1121
  rtnStr = ''
1034
1122
  for output, hosts in outputs.items():
1035
- if global_suppress_printout:
1123
+ if __global_suppress_printout:
1036
1124
  rtnStr += f'Error returncode produced by {hosts}:\n'
1037
1125
  rtnStr += output+'\n'
1038
1126
  else:
1039
1127
  rtnStr += '*'*80+'\n'
1040
1128
  rtnStr += f"These hosts: {hosts} have a response of:\n"
1041
1129
  rtnStr += output+'\n'
1042
- if not global_suppress_printout or outputs:
1130
+ if not __global_suppress_printout or outputs:
1043
1131
  rtnStr += '*'*80+'\n'
1044
- if keyPressesIn[-1]:
1045
- CMDsOut = [''.join(cmd).encode('unicode_escape').decode().replace('\\n', '↵') for cmd in keyPressesIn if cmd]
1046
- #rtnStr += f"Key presses: {''.join(keyPressesIn).encode('unicode_escape').decode()}\n"
1047
- #rtnStr += f"Key presses: {keyPressesIn}\n"
1132
+ if __keyPressesIn[-1]:
1133
+ CMDsOut = [''.join(cmd).encode('unicode_escape').decode().replace('\\n', '↵') for cmd in __keyPressesIn if cmd]
1134
+ #rtnStr += f"Key presses: {''.join(__keyPressesIn).encode('unicode_escape').decode()}\n"
1135
+ #rtnStr += f"Key presses: {__keyPressesIn}\n"
1048
1136
  rtnStr += "User Inputs: \n "
1049
1137
  rtnStr += '\n '.join(CMDsOut)
1050
1138
  rtnStr += '\n'
1051
- keyPressesIn = [[]]
1052
- if global_suppress_printout and not outputs:
1139
+ __keyPressesIn = [[]]
1140
+ if __global_suppress_printout and not outputs:
1053
1141
  rtnStr += 'Success'
1054
1142
  if not quiet:
1055
1143
  print(rtnStr)
@@ -1095,10 +1183,10 @@ def signal_handler(sig, frame):
1095
1183
  Returns:
1096
1184
  None
1097
1185
  '''
1098
- global emo
1099
- if not emo:
1186
+ global _emo
1187
+ if not _emo:
1100
1188
  print('Ctrl C caught, exiting...')
1101
- emo = True
1189
+ _emo = True
1102
1190
  else:
1103
1191
  print('Ctrl C caught again, exiting immediately!')
1104
1192
  # wait for 0.1 seconds to allow the threads to exit
@@ -1107,10 +1195,10 @@ def signal_handler(sig, frame):
1107
1195
  sys.exit(0)
1108
1196
 
1109
1197
 
1110
- def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, quiet, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window = DEFAULT_SINGLE_WINDOW):
1111
- global gloablUnavailableHosts
1198
+ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, nowatch, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,single_window = DEFAULT_SINGLE_WINDOW):
1199
+ global __gloablUnavailableHosts
1112
1200
  threads = start_run_on_hosts(hosts, timeout=timeout,password=password,max_connections=max_connections)
1113
- if not quiet and threads and not returnUnfinished and any([thread.is_alive() for thread in threads]) and sys.stdout.isatty() and os.get_terminal_size() and os.get_terminal_size().columns > 10:
1201
+ if not nowatch and threads and not returnUnfinished and any([thread.is_alive() for thread in threads]) and sys.stdout.isatty() and os.get_terminal_size() and os.get_terminal_size().columns > 10:
1114
1202
  curses.wrapper(curses_print, hosts, threads, min_char_len = curses_min_char_len, min_line_len = curses_min_line_len, single_window = single_window)
1115
1203
  if not returnUnfinished:
1116
1204
  # wait until all hosts have a return code
@@ -1121,7 +1209,7 @@ def processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinishe
1121
1209
  # update the unavailable hosts and global unavailable hosts
1122
1210
  if willUpdateUnreachableHosts:
1123
1211
  unavailableHosts.update([host.name for host in hosts if host.stderr and ('No route to host' in host.stderr[0].strip() or host.stderr[0].strip().startswith('Timeout!'))])
1124
- gloablUnavailableHosts.update(unavailableHosts)
1212
+ __gloablUnavailableHosts.update(unavailableHosts)
1125
1213
  # print the output, if the output of multiple hosts are the same, we aggragate them
1126
1214
  if not called:
1127
1215
  print_output(hosts,json,greppable=greppable)
@@ -1152,7 +1240,7 @@ def formHostStr(host) -> str:
1152
1240
 
1153
1241
  @cache_decorator
1154
1242
  def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
1155
- quiet = DEFAULT_QUIET,json = DEFAULT_JSON_MODE,max_connections=DEFAULT_MAX_CONNECTIONS,
1243
+ nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,max_connections=DEFAULT_MAX_CONNECTIONS,
1156
1244
  files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,
1157
1245
  scp=DEFAULT_SCP,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1158
1246
  no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,skip_hosts = DEFAULT_SKIP_HOSTS,
@@ -1162,9 +1250,9 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
1162
1250
  if oneonone: argsList.append('--oneonone' if not shortend else '-11')
1163
1251
  if timeout and timeout != DEFAULT_TIMEOUT: argsList.append(f'--timeout={timeout}' if not shortend else f'-t={timeout}')
1164
1252
  if password and password != DEFAULT_PASSWORD: argsList.append(f'--password="{password}"' if not shortend else f'-p="{password}"')
1165
- if quiet: argsList.append('--quiet' if not shortend else '-q')
1253
+ if nowatch: argsList.append('--nowatch' if not shortend else '-q')
1166
1254
  if json: argsList.append('--json' if not shortend else '-j')
1167
- if max_connections and max_connections != DEFAULT_MAX_CONNECTIONS: argsList.append(f'--maxconnections={max_connections}' if not shortend else f'-m={max_connections}')
1255
+ if max_connections and max_connections != DEFAULT_MAX_CONNECTIONS: argsList.append(f'--max_connections={max_connections}' if not shortend else f'-m={max_connections}')
1168
1256
  if files: argsList.extend([f'--file="{file}"' for file in files] if not shortend else [f'-f="{file}"' for file in files])
1169
1257
  if ipmi: argsList.append('--ipmi')
1170
1258
  if interface_ip_prefix and interface_ip_prefix != DEFAULT_INTERFACE_IP_PREFIX: argsList.append(f'--interface_ip_prefix="{interface_ip_prefix}"' if not shortend else f'-pre="{interface_ip_prefix}"')
@@ -1179,56 +1267,56 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
1179
1267
  if file_sync: argsList.append('--file_sync' if not shortend else '-fs')
1180
1268
  return ' '.join(argsList)
1181
1269
 
1182
- def getStrCommand(hosts,commands,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
1183
- quiet = DEFAULT_QUIET,json = DEFAULT_JSON_MODE,called = DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
1184
- files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished = DEFAULT_RETURN_UNFINISHED,
1270
+ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
1271
+ nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
1272
+ files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished = _DEFAULT_RETURN_UNFINISHED,
1185
1273
  scp=DEFAULT_SCP,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1186
- no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=DEFAULT_NO_START,
1274
+ no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
1187
1275
  skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
1188
1276
  single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY, shortend = False):
1189
1277
  hosts = hosts if type(hosts) == str else frozenset(hosts)
1190
1278
  hostStr = formHostStr(hosts)
1191
1279
  files = frozenset(files) if files else None
1192
1280
  argsStr = __formCommandArgStr(oneonone = oneonone, timeout = timeout,password = password,
1193
- quiet = quiet,json = json,max_connections=max_connections,
1281
+ nowatch = nowatch,json = json,max_connections=max_connections,
1194
1282
  files = files,ipmi = ipmi,interface_ip_prefix = interface_ip_prefix,scp=scp,
1195
1283
  username=username,extraargs=extraargs,skipUnreachable=skipUnreachable,no_env=no_env,
1196
1284
  greppable=greppable,skip_hosts = skip_hosts, file_sync = file_sync,error_only = error_only, shortend = shortend)
1197
1285
  commandStr = '"' + '" "'.join(commands) + '"' if commands else ''
1198
1286
  return f'multissh {argsStr} {hostStr} {commandStr}'
1199
1287
 
1200
- def run_command_on_hosts(hosts,commands,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
1201
- quiet = DEFAULT_QUIET,json = DEFAULT_JSON_MODE,called = DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
1202
- files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished = DEFAULT_RETURN_UNFINISHED,
1288
+ def run_command_on_hosts(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT,password = DEFAULT_PASSWORD,
1289
+ nowatch = DEFAULT_NO_WATCH,json = DEFAULT_JSON_MODE,called = _DEFAULT_CALLED,max_connections=DEFAULT_MAX_CONNECTIONS,
1290
+ files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished = _DEFAULT_RETURN_UNFINISHED,
1203
1291
  scp=DEFAULT_SCP,username=DEFAULT_USERNAME,extraargs=DEFAULT_EXTRA_ARGS,skipUnreachable=DEFAULT_SKIP_UNREACHABLE,
1204
- no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=DEFAULT_NO_START,
1292
+ no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,willUpdateUnreachableHosts=_DEFAULT_UPDATE_UNREACHABLE_HOSTS,no_start=_DEFAULT_NO_START,
1205
1293
  skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
1206
1294
  single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY):
1207
1295
  f'''
1208
1296
  Run the command on the hosts, aka multissh. main function
1209
1297
 
1210
1298
  Args:
1211
- hosts (str/iterable): A string of hosts seperated by space or comma / iterable of hosts.
1212
- commands (list): A list of commands to run on the hosts. When using files, defines the destination of the files.
1299
+ hosts (str/iterable): A string of hosts seperated by space or comma / iterable of hosts. Default to {DEFAULT_HOSTS}.
1300
+ commands (list): A list of commands to run on the hosts. When using files, defines the destination of the files. Defaults to None.
1213
1301
  oneonone (bool, optional): Whether to run the commands one on one. Defaults to {DEFAULT_ONE_ON_ONE}.
1214
1302
  timeout (int, optional): The timeout for the command. Defaults to {DEFAULT_TIMEOUT}.
1215
1303
  password (str, optional): The password for the hosts. Defaults to {DEFAULT_PASSWORD}.
1216
- quiet (bool, optional): Whether to print the output. Defaults to {DEFAULT_QUIET}.
1304
+ nowatch (bool, optional): Whether to print the output. Defaults to {DEFAULT_NO_WATCH}.
1217
1305
  json (bool, optional): Whether to print the output in JSON format. Defaults to {DEFAULT_JSON_MODE}.
1218
- called (bool, optional): Whether the function is called by another function. Defaults to {DEFAULT_CALLED}.
1306
+ called (bool, optional): Whether the function is called by another function. Defaults to {_DEFAULT_CALLED}.
1219
1307
  max_connections (int, optional): The maximum number of concurrent SSH sessions. Defaults to 4 * os.cpu_count().
1220
1308
  files (list, optional): A list of files to be copied to the hosts. Defaults to None.
1221
1309
  ipmi (bool, optional): Whether to use IPMI to connect to the hosts. Defaults to {DEFAULT_IPMI}.
1222
1310
  interface_ip_prefix (str, optional): The prefix of the IPMI interface. Defaults to {DEFAULT_INTERFACE_IP_PREFIX}.
1223
- returnUnfinished (bool, optional): Whether to return the unfinished hosts. Defaults to {DEFAULT_RETURN_UNFINISHED}.
1311
+ returnUnfinished (bool, optional): Whether to return the unfinished hosts. Defaults to {_DEFAULT_RETURN_UNFINISHED}.
1224
1312
  scp (bool, optional): Whether to use scp instead of rsync. Defaults to {DEFAULT_SCP}.
1225
1313
  username (str, optional): The username to use to connect to the hosts. Defaults to {DEFAULT_USERNAME}.
1226
1314
  extraargs (str, optional): Extra arguments to pass to the ssh / rsync / scp command. Defaults to {DEFAULT_EXTRA_ARGS}.
1227
1315
  skipUnreachable (bool, optional): Whether to skip unreachable hosts. Defaults to {DEFAULT_SKIP_UNREACHABLE}.
1228
1316
  no_env (bool, optional): Whether to not read the current sat system environment variables. (Will still read from files) Defaults to {DEFAULT_NO_ENV}.
1229
1317
  greppable (bool, optional): Whether to print the output in greppable format. Defaults to {DEFAULT_GREPPABLE_MODE}.
1230
- willUpdateUnreachableHosts (bool, optional): Whether to update the global unavailable hosts. Defaults to {DEFAULT_UPDATE_UNREACHABLE_HOSTS}.
1231
- no_start (bool, optional): Whether to return the hosts without starting the command. Defaults to {DEFAULT_NO_START}.
1318
+ willUpdateUnreachableHosts (bool, optional): Whether to update the global unavailable hosts. Defaults to {_DEFAULT_UPDATE_UNREACHABLE_HOSTS}.
1319
+ no_start (bool, optional): Whether to return the hosts without starting the command. Defaults to {_DEFAULT_NO_START}.
1232
1320
  skip_hosts (str, optional): The hosts to skip. Defaults to {DEFAULT_SKIP_HOSTS}.
1233
1321
  min_char_len (int, optional): The minimum character per line of the curses output. Defaults to {DEFAULT_CURSES_MINIMUM_CHAR_LEN}.
1234
1322
  min_line_len (int, optional): The minimum line number for each window of the curses output. Defaults to {DEFAULT_CURSES_MINIMUM_LINE_LEN}.
@@ -1238,8 +1326,8 @@ def run_command_on_hosts(hosts,commands,oneonone = DEFAULT_ONE_ON_ONE, timeout =
1238
1326
  Returns:
1239
1327
  list: A list of Host objects
1240
1328
  '''
1241
- global gloablUnavailableHosts
1242
- global global_suppress_printout
1329
+ global __gloablUnavailableHosts
1330
+ global __global_suppress_printout
1243
1331
  if not max_connections:
1244
1332
  max_connections = 4 * os.cpu_count()
1245
1333
  elif max_connections == 0:
@@ -1253,22 +1341,22 @@ def run_command_on_hosts(hosts,commands,oneonone = DEFAULT_ONE_ON_ONE, timeout =
1253
1341
  if called:
1254
1342
  # if called,
1255
1343
  # if skipUnreachable is not set, we default to skip unreachable hosts within one command call
1256
- global_suppress_printout = True
1344
+ __global_suppress_printout = True
1257
1345
  if skipUnreachable is None:
1258
1346
  skipUnreachable = True
1259
1347
  if skipUnreachable:
1260
- unavailableHosts = gloablUnavailableHosts
1348
+ unavailableHosts = __gloablUnavailableHosts
1261
1349
  else:
1262
1350
  unavailableHosts = set()
1263
1351
  else:
1264
1352
  # if run in command line ( or emulating running in command line, we default to skip unreachable hosts within one command call )
1265
1353
  if skipUnreachable:
1266
- unavailableHosts = gloablUnavailableHosts
1354
+ unavailableHosts = __gloablUnavailableHosts
1267
1355
  else:
1268
1356
  unavailableHosts = set()
1269
1357
  skipUnreachable = True
1270
- global emo
1271
- emo = False
1358
+ global _emo
1359
+ _emo = False
1272
1360
  # We create the hosts
1273
1361
  hostStr = formHostStr(hosts)
1274
1362
  skipHostStr = formHostStr(skip_hosts) if skip_hosts else ''
@@ -1290,7 +1378,7 @@ def run_command_on_hosts(hosts,commands,oneonone = DEFAULT_ONE_ON_ONE, timeout =
1290
1378
  targetHostsList = expand_hostnames(frozenset(hostStr.split(',')),no_env=no_env)
1291
1379
  skipHostsList = expand_hostnames(frozenset(skipHostStr.split(',')),no_env=no_env)
1292
1380
  if skipHostsList:
1293
- if not global_suppress_printout: print(f"Skipping hosts: {skipHostsList}")
1381
+ if not __global_suppress_printout: print(f"Skipping hosts: {skipHostsList}")
1294
1382
  if files and not commands:
1295
1383
  # if files are specified but not target dir, we default to file sync mode
1296
1384
  file_sync = True
@@ -1321,22 +1409,22 @@ def run_command_on_hosts(hosts,commands,oneonone = DEFAULT_ONE_ON_ONE, timeout =
1321
1409
  print(f"Number of commands: {len(commands)}")
1322
1410
  print(f"Number of hosts: {len(targetHostsList - skipHostsList)}")
1323
1411
  sys.exit(255)
1324
- if not global_suppress_printout:
1412
+ if not __global_suppress_printout:
1325
1413
  print('-'*80)
1326
1414
  print("Running in one on one mode")
1327
1415
  for host, command in zip(targetHostsList, commands):
1328
1416
  if not ipmi and skipUnreachable and host.strip() in unavailableHosts:
1329
- if not global_suppress_printout: print(f"Skipping unavailable host: {host}")
1417
+ if not __global_suppress_printout: print(f"Skipping unavailable host: {host}")
1330
1418
  continue
1331
1419
  if host.strip() in skipHostsList: continue
1332
1420
  if file_sync:
1333
1421
  hosts.append(Host(host.strip(), os.path.dirname(command)+os.path.sep, files = [command],ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs))
1334
1422
  else:
1335
1423
  hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs))
1336
- if not global_suppress_printout:
1424
+ if not __global_suppress_printout:
1337
1425
  print(f"Running command: {command} on host: {host}")
1338
- if not global_suppress_printout: print('-'*80)
1339
- if not no_start: processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, quiet, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,single_window=single_window)
1426
+ if not __global_suppress_printout: print('-'*80)
1427
+ if not no_start: processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, nowatch, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,single_window=single_window)
1340
1428
  return hosts
1341
1429
  else:
1342
1430
  allHosts = []
@@ -1345,7 +1433,7 @@ def run_command_on_hosts(hosts,commands,oneonone = DEFAULT_ONE_ON_ONE, timeout =
1345
1433
  hosts = []
1346
1434
  for host in targetHostsList:
1347
1435
  if not ipmi and skipUnreachable and host.strip() in unavailableHosts:
1348
- if not global_suppress_printout: print(f"Skipping unavailable host: {host}")
1436
+ if not __global_suppress_printout: print(f"Skipping unavailable host: {host}")
1349
1437
  continue
1350
1438
  if host.strip() in skipHostsList: continue
1351
1439
  if file_sync:
@@ -1357,93 +1445,164 @@ def run_command_on_hosts(hosts,commands,oneonone = DEFAULT_ONE_ON_ONE, timeout =
1357
1445
  print(f"Error: ipmi mode is not supported in interactive mode")
1358
1446
  else:
1359
1447
  hosts.append(Host(host.strip(), '', files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs))
1360
- if not global_suppress_printout:
1448
+ if not __global_suppress_printout:
1361
1449
  print('-'*80)
1362
1450
  print(f"Running in interactive mode on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
1363
1451
  print('-'*80)
1364
1452
  if no_start:
1365
1453
  print(f"Warning: no_start is set, the command will not be started. As we are in interactive mode, no action will be done.")
1366
1454
  else:
1367
- processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, quiet, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,single_window=single_window)
1455
+ processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, nowatch, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,single_window=single_window)
1368
1456
  return hosts
1369
1457
  for command in commands:
1370
1458
  hosts = []
1371
1459
  for host in targetHostsList:
1372
1460
  if not ipmi and skipUnreachable and host.strip() in unavailableHosts:
1373
- if not global_suppress_printout: print(f"Skipping unavailable host: {host}")
1461
+ if not __global_suppress_printout: print(f"Skipping unavailable host: {host}")
1374
1462
  continue
1375
1463
  if host.strip() in skipHostsList: continue
1376
1464
  if file_sync:
1377
1465
  hosts.append(Host(host.strip(), os.path.dirname(command)+os.path.sep, files = [command],ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs))
1378
1466
  else:
1379
1467
  hosts.append(Host(host.strip(), command, files = files,ipmi=ipmi,interface_ip_prefix=interface_ip_prefix,scp=scp,extraargs=extraargs))
1380
- if not global_suppress_printout and len(commands) > 1:
1468
+ if not __global_suppress_printout and len(commands) > 1:
1381
1469
  print('-'*80)
1382
1470
  print(f"Running command: {command} on hosts: {hostStr}" + (f"; skipping: {skipHostStr}" if skipHostStr else ''))
1383
1471
  print('-'*80)
1384
- if not no_start: processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, quiet, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,single_window=single_window)
1472
+ if not no_start: processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished, nowatch, json, called, greppable,unavailableHosts,willUpdateUnreachableHosts,curses_min_char_len = curses_min_char_len, curses_min_line_len = curses_min_line_len,single_window=single_window)
1385
1473
  allHosts += hosts
1386
1474
  return allHosts
1387
1475
 
1388
- def main():
1389
- global emo
1390
- global global_suppress_printout
1391
- global gloablUnavailableHosts
1392
- global mainReturnCode
1393
- global failedHosts
1394
- global keyPressesIn
1395
- global ipmiiInterfaceIPPrefix
1396
- global sshpassAvailable
1397
- global env_file
1398
- emo = False
1476
+ def get_default_config(args):
1477
+ '''
1478
+ Get the default config
1479
+
1480
+ Args:
1481
+ args (argparse.Namespace): The arguments
1399
1482
 
1483
+ Returns:
1484
+ dict: The default config
1485
+ '''
1486
+ return {
1487
+ 'AUTHOR': AUTHOR,
1488
+ 'AUTHOR_EMAIL': AUTHOR_EMAIL,
1489
+ 'DEFAULT_HOSTS': args.hosts,
1490
+ 'DEFAULT_USERNAME': args.username,
1491
+ 'DEFAULT_PASSWORD': args.password,
1492
+ 'DEFAULT_EXTRA_ARGS': args.extraargs,
1493
+ 'DEFAULT_ONE_ON_ONE': args.oneonone,
1494
+ 'DEFAULT_SCP': args.scp,
1495
+ 'DEFAULT_FILE_SYNC': args.file_sync,
1496
+ 'DEFAULT_TIMEOUT': DEFAULT_TIMEOUT,
1497
+ 'DEFAULT_CLI_TIMEOUT': args.timeout,
1498
+ 'DEFAULT_REPEAT': args.repeat,
1499
+ 'DEFAULT_INTERVAL': args.interval,
1500
+ 'DEFAULT_IPMI': args.ipmi,
1501
+ 'DEFAULT_IPMI_INTERFACE_IP_PREFIX': args.ipmi_interface_ip_prefix,
1502
+ 'DEFAULT_INTERFACE_IP_PREFIX': args.interface_ip_prefix,
1503
+ 'DEFAULT_NO_WATCH': args.nowatch,
1504
+ 'DEFAULT_CURSES_MINIMUM_CHAR_LEN': args.window_width,
1505
+ 'DEFAULT_CURSES_MINIMUM_LINE_LEN': args.window_height,
1506
+ 'DEFAULT_SINGLE_WINDOW': args.single_window,
1507
+ 'DEFAULT_ERROR_ONLY': args.error_only,
1508
+ 'DEFAULT_NO_OUTPUT': args.no_output,
1509
+ 'DEFAULT_NO_ENV': args.no_env,
1510
+ 'DEFAULT_ENV_FILE': args.env_file,
1511
+ 'DEFAULT_MAX_CONNECTIONS': args.max_connections if args.max_connections != 4 * os.cpu_count() else None,
1512
+ 'DEFAULT_JSON_MODE': args.json,
1513
+ 'DEFAULT_PRINT_SUCCESS_HOSTS': args.success_hosts,
1514
+ 'DEFAULT_GREPPABLE_MODE': args.greppable,
1515
+ 'DEFAULT_SKIP_UNREACHABLE': args.skip_unreachable,
1516
+ 'DEFAULT_SKIP_HOSTS': args.skip_hosts,
1517
+ 'ERROR_MESSAGES_TO_IGNORE': ERROR_MESSAGES_TO_IGNORE,
1518
+ }
1519
+
1520
+ def write_default_config(args,CONFIG_FILE,backup = True):
1521
+ if backup and os.path.exists(CONFIG_FILE):
1522
+ os.rename(CONFIG_FILE,CONFIG_FILE+'.bak')
1523
+ default_config = get_default_config(args)
1524
+ with open(CONFIG_FILE,'w') as f:
1525
+ json.dump(default_config,f,indent=4)
1400
1526
 
1527
+
1528
+ def main():
1529
+ global _emo
1530
+ global __global_suppress_printout
1531
+ global __gloablUnavailableHosts
1532
+ global __mainReturnCode
1533
+ global __failedHosts
1534
+ global __ipmiiInterfaceIPPrefix
1535
+ global _sshpassAvailable
1536
+ global _env_file
1537
+ _emo = False
1401
1538
  # We handle the signal
1402
1539
  signal.signal(signal.SIGINT, signal_handler)
1403
1540
  # We parse the arguments
1404
- parser = argparse.ArgumentParser(description='Run a command on multiple hosts, Use #HOST# or #HOSTNAME# to replace the host name in the command')
1405
- parser.add_argument('hosts', metavar='hosts', type=str, help='Hosts to run the command on, use "," to seperate hosts')
1406
- parser.add_argument('commands', metavar='commands', type=str, nargs='+',help='the command to run on the hosts / the destination of the files #HOST# or #HOSTNAME# will be replaced with the host name.')
1541
+ parser = argparse.ArgumentParser(description=f'Run a command on multiple hosts, Use #HOST# or #HOSTNAME# to replace the host name in the command. Config file: {CONFIG_FILE}')
1542
+ 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)
1543
+ 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.')
1407
1544
  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)
1408
- 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)
1409
1545
  parser.add_argument('-p', '--password', type=str,help=f'The password to use to connect to the hosts, (default: {DEFAULT_PASSWORD})',default=DEFAULT_PASSWORD)
1546
+ 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)
1410
1547
  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)
1411
1548
  parser.add_argument("-f","--file", action='append', help="The file to be copied to the hosts. Use -f multiple times to copy multiple files")
1412
1549
  parser.add_argument('--file_sync', action='store_true', help=f'Operate in file sync mode, sync path in <COMMANDS> from this machine to <HOSTS>. Treat --file <FILE> and <COMMANDS> both as source as source and destination will be the same in this mode. (default: {DEFAULT_FILE_SYNC})', default=DEFAULT_FILE_SYNC)
1413
1550
  parser.add_argument('--scp', action='store_true', help=f'Use scp for copying files instead of rsync. Need to use this on windows. (default: {DEFAULT_SCP})', default=DEFAULT_SCP)
1414
1551
  #parser.add_argument("-d",'-c',"--destination", type=str, help="The destination of the files. Same as specify with commands. Added for compatibility. Use #HOST# or #HOSTNAME# to replace the host name in the destination")
1415
- parser.add_argument("-t","--timeout", type=int, help=f"Timeout for each command in seconds (default: 0 (disabled))", default=0)
1552
+ parser.add_argument("-t","--timeout", type=int, help=f"Timeout for each command in seconds (default: {DEFAULT_CLI_TIMEOUT} (disabled))", default=DEFAULT_CLI_TIMEOUT)
1416
1553
  parser.add_argument("-r","--repeat", type=int, help=f"Repeat the command for a number of times (default: {DEFAULT_REPEAT})", default=DEFAULT_REPEAT)
1417
1554
  parser.add_argument("-i","--interval", type=int, help=f"Interval between repeats in seconds (default: {DEFAULT_INTERVAL})", default=DEFAULT_INTERVAL)
1418
1555
  parser.add_argument("--ipmi", action='store_true', help=f"Use ipmitool to run the command. (default: {DEFAULT_IPMI})", default=DEFAULT_IPMI)
1419
1556
  parser.add_argument("-mpre","--ipmi_interface_ip_prefix", type=str, help=f"The prefix of the IPMI interfaces (default: {DEFAULT_IPMI_INTERFACE_IP_PREFIX})", default=DEFAULT_IPMI_INTERFACE_IP_PREFIX)
1420
1557
  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)
1421
- parser.add_argument("-q","--quiet", action='store_true', help=f"Quiet mode, no curses, only print the output. (default: {DEFAULT_QUIET})", default=DEFAULT_QUIET)
1558
+ parser.add_argument("-q","-nw","--nowatch","--quiet", action='store_true', help=f"Quiet mode, no curses watch, only print the output. (default: {DEFAULT_NO_WATCH})", default=DEFAULT_NO_WATCH)
1422
1559
  parser.add_argument("-ww",'--window_width', type=int, help=f"The minimum character length of the curses window. (default: {DEFAULT_CURSES_MINIMUM_CHAR_LEN})", default=DEFAULT_CURSES_MINIMUM_CHAR_LEN)
1423
1560
  parser.add_argument("-wh",'--window_height', type=int, help=f"The minimum line height of the curses window. (default: {DEFAULT_CURSES_MINIMUM_LINE_LEN})", default=DEFAULT_CURSES_MINIMUM_LINE_LEN)
1424
1561
  parser.add_argument('-sw','--single_window', action='store_true', help=f'Use a single window for all hosts. (default: {DEFAULT_SINGLE_WINDOW})', default=DEFAULT_SINGLE_WINDOW)
1425
1562
  parser.add_argument('-eo','--error_only', action='store_true', help=f'Only print the error output. (default: {DEFAULT_ERROR_ONLY})', default=DEFAULT_ERROR_ONLY)
1426
- parser.add_argument("-no","--nooutput", action='store_true', help=f"Do not print the output. (default: {DEFAULT_NO_OUTPUT})", default=DEFAULT_NO_OUTPUT)
1563
+ parser.add_argument("-no","--no_output", action='store_true', help=f"Do not print the output. (default: {DEFAULT_NO_OUTPUT})", default=DEFAULT_NO_OUTPUT)
1427
1564
  parser.add_argument('--no_env', action='store_true', help=f'Do not load the environment variables. (default: {DEFAULT_NO_ENV})', default=DEFAULT_NO_ENV)
1428
1565
  parser.add_argument("--env_file", type=str, help=f"The file to load the environment variables from. (default: {DEFAULT_ENV_FILE})", default=DEFAULT_ENV_FILE)
1429
- parser.add_argument("-m","--maxconnections", type=int, help=f"Max number of connections to use (default: 4 * cpu_count)", default=DEFAULT_MAX_CONNECTIONS)
1566
+ parser.add_argument("-m","--max_connections", type=int, help=f"Max number of connections to use (default: 4 * cpu_count)", default=DEFAULT_MAX_CONNECTIONS)
1430
1567
  parser.add_argument("-j","--json", action='store_true', help=F"Output in json format. (default: {DEFAULT_JSON_MODE})", default=DEFAULT_JSON_MODE)
1431
1568
  parser.add_argument("--success_hosts", action='store_true', help=f"Output the hosts that succeeded in summary as wells. (default: {DEFAULT_PRINT_SUCCESS_HOSTS})", default=DEFAULT_PRINT_SUCCESS_HOSTS)
1432
1569
  parser.add_argument("-g","--greppable", action='store_true', help=f"Output in greppable format. (default: {DEFAULT_GREPPABLE_MODE})", default=DEFAULT_GREPPABLE_MODE)
1433
- parser.add_argument("-nw","--nowatch", action='store_true', help=f"Do not watch the output in curses modem, Use \\r. Not implemented yet. (default: {DEFAULT_NO_WATCH})", default=DEFAULT_NO_WATCH)
1434
- parser.add_argument("-su","--skipunreachable", action='store_true', help=f"Skip unreachable hosts while using --repeat. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {DEFAULT_SKIP_UNREACHABLE})", default=DEFAULT_SKIP_UNREACHABLE)
1435
- parser.add_argument("-sh","--skiphosts", type=str, help=f"Skip the hosts in the list. (default: {DEFAULT_SKIP_HOSTS})", default=DEFAULT_SKIP_HOSTS)
1436
- parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} {("with sshpass " if sshpassAvailable else "")}by pan@zopyr.us')
1570
+ parser.add_argument("-su","--skip_unreachable", action='store_true', help=f"Skip unreachable hosts while using --repeat. Note: Timedout Hosts are considered unreachable. Note: multiple command sequence will still auto skip unreachable hosts. (default: {DEFAULT_SKIP_UNREACHABLE})", default=DEFAULT_SKIP_UNREACHABLE)
1571
+ parser.add_argument("-sh","--skip_hosts", type=str, help=f"Skip the hosts in the list. (default: {DEFAULT_SKIP_HOSTS if DEFAULT_SKIP_HOSTS else 'None'})", default=DEFAULT_SKIP_HOSTS)
1572
+ parser.add_argument('--generate_default_config_file', action='store_true', help=f'Generate / store the default config file from command line argument and current config at {CONFIG_FILE}')
1573
+ parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} {("with sshpass " if _sshpassAvailable else "")}by {AUTHOR} ({AUTHOR_EMAIL})')
1437
1574
 
1438
1575
  # parser.add_argument('-u', '--user', metavar='user', type=str, nargs=1,
1439
1576
  # help='the user to use to connect to the hosts')
1440
1577
  args = parser.parse_args()
1441
1578
 
1442
- env_file = args.env_file
1579
+ if args.generate_default_config_file:
1580
+ try:
1581
+ if os.path.exists(CONFIG_FILE):
1582
+ print(f"Warning: {CONFIG_FILE} already exists, what to do? (o/b/n)")
1583
+ print(f"o: Overwrite the file")
1584
+ print(f"b: Rename the current config file at {CONFIG_FILE}.bak forcefully and write the new config file (default)")
1585
+ print(f"n: Do nothing")
1586
+ inStr = input_with_timeout_and_countdown(10)
1587
+ if (not inStr) or inStr.lower().strip().startswith('b'):
1588
+ write_default_config(args,CONFIG_FILE,backup = True)
1589
+ print(f"Config file written to {CONFIG_FILE}")
1590
+ elif inStr.lower().strip().startswith('o'):
1591
+ write_default_config(args,CONFIG_FILE,backup = False)
1592
+ print(f"Config file written to {CONFIG_FILE}")
1593
+ else:
1594
+ write_default_config(args,CONFIG_FILE,backup = True)
1595
+ print(f"Config file written to {CONFIG_FILE}")
1596
+ except Exception as e:
1597
+ print(f"Error while writing config file: {e}")
1598
+ if not args.commands:
1599
+ sys.exit(0)
1600
+
1601
+ _env_file = args.env_file
1443
1602
  # if there are more than 1 commands, and every command only consists of one word,
1444
1603
  # we will ask the user to confirm if they want to run multiple commands or just one command.
1445
1604
  if not args.file and len(args.commands) > 1 and all([len(command.split()) == 1 for command in args.commands]):
1446
- print(f"Multiple one word command detected, what to do? (s/f/n)")
1605
+ print(f"Multiple one word command detected, what to do? (1/m/n)")
1447
1606
  print(f"1: Run 1 command [{' '.join(args.commands)}] on all hosts ( default )")
1448
1607
  print(f"m: Run multiple commands [{', '.join(args.commands)}] on all hosts")
1449
1608
  print(f"n: Exit")
@@ -1456,66 +1615,64 @@ def main():
1456
1615
  else:
1457
1616
  sys.exit(0)
1458
1617
 
1459
- ipmiiInterfaceIPPrefix = args.ipmi_interface_ip_prefix
1618
+ __ipmiiInterfaceIPPrefix = args.ipmi_interface_ip_prefix
1460
1619
 
1461
- if not args.greppable and not args.json and not args.nooutput:
1462
- global_suppress_printout = False
1620
+ if not args.greppable and not args.json and not args.no_output:
1621
+ __global_suppress_printout = False
1463
1622
 
1464
- if not global_suppress_printout:
1623
+ if not __global_suppress_printout:
1465
1624
  print('> ' + getStrCommand(args.hosts,args.commands,oneonone=args.oneonone,timeout=args.timeout,password=args.password,
1466
- quiet=args.quiet,json=args.json,called=args.nooutput,max_connections=args.maxconnections,
1625
+ nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
1467
1626
  files=args.file,file_sync=args.file_sync,ipmi=args.ipmi,interface_ip_prefix=args.interface_ip_prefix,scp=args.scp,username=args.username,
1468
- extraargs=args.extraargs,skipUnreachable=args.skipunreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skiphosts,
1627
+ extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
1469
1628
  curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only))
1470
1629
  if args.error_only:
1471
- global_suppress_printout = True
1630
+ __global_suppress_printout = True
1472
1631
 
1473
1632
  for i in range(args.repeat):
1474
1633
  if args.interval > 0 and i < args.repeat - 1:
1475
1634
  print(f"Sleeping for {args.interval} seconds")
1476
1635
  time.sleep(args.interval)
1477
1636
 
1478
- if not global_suppress_printout: print(f"Running the {i+1}/{args.repeat} time") if args.repeat > 1 else None
1637
+ if not __global_suppress_printout: print(f"Running the {i+1}/{args.repeat} time") if args.repeat > 1 else None
1479
1638
  hosts = run_command_on_hosts(args.hosts,args.commands,
1480
1639
  oneonone=args.oneonone,timeout=args.timeout,password=args.password,
1481
- quiet=args.quiet,json=args.json,called=args.nooutput,max_connections=args.maxconnections,
1640
+ nowatch=args.nowatch,json=args.json,called=args.no_output,max_connections=args.max_connections,
1482
1641
  files=args.file,file_sync=args.file_sync,ipmi=args.ipmi,interface_ip_prefix=args.interface_ip_prefix,scp=args.scp,username=args.username,
1483
- extraargs=args.extraargs,skipUnreachable=args.skipunreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skiphosts,
1642
+ extraargs=args.extraargs,skipUnreachable=args.skip_unreachable,no_env=args.no_env,greppable=args.greppable,skip_hosts = args.skip_hosts,
1484
1643
  curses_min_char_len = args.window_width, curses_min_line_len = args.window_height,single_window=args.single_window,error_only=args.error_only)
1485
1644
  #print('*'*80)
1486
1645
 
1487
- if not global_suppress_printout: print('-'*80)
1646
+ if not __global_suppress_printout: print('-'*80)
1488
1647
 
1489
1648
  succeededHosts = set()
1490
1649
  for host in hosts:
1491
1650
  if host.returncode and host.returncode != 0:
1492
- mainReturnCode += 1
1493
- failedHosts.add(host.name)
1651
+ __mainReturnCode += 1
1652
+ __failedHosts.add(host.name)
1494
1653
  else:
1495
1654
  succeededHosts.add(host.name)
1496
- succeededHosts -= failedHosts
1655
+ succeededHosts -= __failedHosts
1497
1656
  # sort the failed hosts and succeeded hosts
1498
- failedHosts = sorted(failedHosts)
1657
+ __failedHosts = sorted(__failedHosts)
1499
1658
  succeededHosts = sorted(succeededHosts)
1500
- if mainReturnCode > 0:
1501
- if not global_suppress_printout: print(f'Complete. Failed hosts (Return Code not 0) count: {mainReturnCode}')
1659
+ if __mainReturnCode > 0:
1660
+ if not __global_suppress_printout: print(f'Complete. Failed hosts (Return Code not 0) count: {__mainReturnCode}')
1502
1661
  # with open('/tmp/bashcmd.stdin','w') as f:
1503
- # f.write(f"export failed_hosts={failedHosts}\n")
1504
- if not global_suppress_printout: print(f'failed_hosts: {",".join(failedHosts)}')
1662
+ # f.write(f"export failed_hosts={__failedHosts}\n")
1663
+ if not __global_suppress_printout: print(f'failed_hosts: {",".join(__failedHosts)}')
1505
1664
  else:
1506
- if not global_suppress_printout: print('Complete. All hosts returned 0.')
1665
+ if not __global_suppress_printout: print('Complete. All hosts returned 0.')
1507
1666
 
1508
- if args.success_hosts and not global_suppress_printout:
1667
+ if args.success_hosts and not __global_suppress_printout:
1509
1668
  print(f'succeeded_hosts: {",".join(succeededHosts)}')
1510
1669
 
1511
1670
  if threading.active_count() > 1:
1512
- if not global_suppress_printout: print(f'Remaining active thread: {threading.active_count()}')
1671
+ if not __global_suppress_printout: print(f'Remaining active thread: {threading.active_count()}')
1513
1672
  # os.system(f'pkill -ef {os.path.basename(__file__)}')
1514
1673
  # os._exit(mainReturnCode)
1515
1674
 
1516
- sys.exit(mainReturnCode)
1517
-
1518
-
1675
+ sys.exit(__mainReturnCode)
1519
1676
 
1520
1677
  if __name__ == "__main__":
1521
1678
  main()
@@ -1,7 +0,0 @@
1
- multiSSH3.py,sha256=h28zfJKS3LlcKWdp9DAJWF7VcxbYMKA8WAS8Va1Rz9s,72520
2
- multiSSH3-4.76.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
3
- multiSSH3-4.76.dist-info/METADATA,sha256=oadx4ocFNxLO6vspArzIyeLEsGoifyjq10w7YHEVybc,15482
4
- multiSSH3-4.76.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
5
- multiSSH3-4.76.dist-info/entry_points.txt,sha256=xi2rWWNfmHx6gS8Mmx0rZL2KZz6XWBYP3DWBpWAnnZ0,143
6
- multiSSH3-4.76.dist-info/top_level.txt,sha256=tUwttxlnpLkZorSsroIprNo41lYSxjd2ASuL8-EJIJw,10
7
- multiSSH3-4.76.dist-info/RECORD,,