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.
- {multiSSH3-4.76.dist-info → multiSSH3-4.81.dist-info}/METADATA +65 -23
- multiSSH3-4.81.dist-info/RECORD +7 -0
- {multiSSH3-4.76.dist-info → multiSSH3-4.81.dist-info}/WHEEL +1 -1
- multiSSH3.py +370 -213
- multiSSH3-4.76.dist-info/RECORD +0 -7
- {multiSSH3-4.76.dist-info → multiSSH3-4.81.dist-info}/LICENSE +0 -0
- {multiSSH3-4.76.dist-info → multiSSH3-4.81.dist-info}/entry_points.txt +0 -0
- {multiSSH3-4.76.dist-info → multiSSH3-4.81.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: multiSSH3
|
|
3
|
-
Version: 4.
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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 ```
|
|
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
|
-
- [
|
|
125
|
-
- [
|
|
126
|
-
- [
|
|
127
|
-
- [
|
|
128
|
-
- [
|
|
129
|
-
- [
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
- [
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
- [
|
|
139
|
-
- [
|
|
140
|
-
- [
|
|
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,,
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
177
|
+
__wildCharacters = ['*','?','x']
|
|
94
178
|
|
|
95
|
-
|
|
179
|
+
__gloablUnavailableHosts = set()
|
|
96
180
|
|
|
97
|
-
|
|
181
|
+
__ipmiiInterfaceIPPrefix = DEFAULT_IPMI_INTERFACE_IP_PREFIX
|
|
98
182
|
|
|
99
|
-
|
|
183
|
+
__keyPressesIn = [[]]
|
|
100
184
|
|
|
101
|
-
|
|
185
|
+
_emo = False
|
|
102
186
|
|
|
103
|
-
|
|
187
|
+
_etc_hosts = __configs_from_file.get('_etc_hosts', __build_in_default_config['_etc_hosts'])
|
|
104
188
|
|
|
105
|
-
|
|
189
|
+
_env_file = DEFAULT_ENV_FILE
|
|
106
190
|
|
|
107
191
|
# check if command sshpass is available
|
|
108
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
258
|
+
global _env_file
|
|
175
259
|
if environemnt_file:
|
|
176
260
|
envf = environemnt_file
|
|
177
261
|
else:
|
|
178
|
-
envf =
|
|
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
|
|
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
|
|
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
|
-
|
|
223
|
-
if hostname in
|
|
224
|
-
return
|
|
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
|
|
379
|
-
|
|
380
|
-
global
|
|
381
|
-
|
|
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
|
|
462
|
-
#
|
|
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(
|
|
468
|
-
stream.write(''.join(
|
|
551
|
+
if sentInput < len(__keyPressesIn) - 1 :
|
|
552
|
+
stream.write(''.join(__keyPressesIn[sentInput]).encode())
|
|
469
553
|
stream.flush()
|
|
470
|
-
host.output.append(' $ ' + ''.join(
|
|
471
|
-
host.stdout.append(' $ ' + ''.join(
|
|
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(
|
|
476
|
-
print(f"Warning: {len(
|
|
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
|
|
479
|
-
# stream.write(''.join(
|
|
562
|
+
# if __keyPressesIn and __keyPressesIn[-1]:
|
|
563
|
+
# stream.write(''.join(__keyPressesIn[-1]).encode())
|
|
480
564
|
# stream.flush()
|
|
481
|
-
# host.output.append(' $ ' + ''.join(
|
|
482
|
-
# host.stdout.append(' $ ' + ''.join(
|
|
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
|
|
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
|
|
518
|
-
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
|
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
|
|
554
|
-
#
|
|
555
|
-
#
|
|
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
|
|
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
|
|
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
|
|
631
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
921
|
+
__keyPressesIn[-1] = __keyPressesIn[lineToDisplay][:-1]
|
|
834
922
|
lineToDisplay = -1
|
|
835
|
-
curserPosition = max(0, min(curserPosition, len(
|
|
923
|
+
curserPosition = max(0, min(curserPosition, len(__keyPressesIn[lineToDisplay])))
|
|
836
924
|
if key == 10: # 10 is the key code for newline
|
|
837
|
-
|
|
838
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
854
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
989
|
-
global
|
|
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
|
|
1013
|
-
CMDsOut = [''.join(cmd).encode('unicode_escape').decode().replace('\\n', '↵') for cmd in
|
|
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
|
|
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
|
|
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
|
|
1130
|
+
if not __global_suppress_printout or outputs:
|
|
1043
1131
|
rtnStr += '*'*80+'\n'
|
|
1044
|
-
if
|
|
1045
|
-
CMDsOut = [''.join(cmd).encode('unicode_escape').decode().replace('\\n', '↵') for cmd in
|
|
1046
|
-
#rtnStr += f"Key presses: {''.join(
|
|
1047
|
-
#rtnStr += f"Key presses: {
|
|
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
|
-
|
|
1052
|
-
if
|
|
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
|
|
1099
|
-
if not
|
|
1186
|
+
global _emo
|
|
1187
|
+
if not _emo:
|
|
1100
1188
|
print('Ctrl C caught, exiting...')
|
|
1101
|
-
|
|
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,
|
|
1111
|
-
global
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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'--
|
|
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
|
-
|
|
1184
|
-
files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished =
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
1202
|
-
files = None,ipmi = DEFAULT_IPMI,interface_ip_prefix = DEFAULT_INTERFACE_IP_PREFIX,returnUnfinished =
|
|
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=
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
1231
|
-
no_start (bool, optional): Whether to return the hosts without starting the command. Defaults to {
|
|
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
|
|
1242
|
-
global
|
|
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
|
-
|
|
1344
|
+
__global_suppress_printout = True
|
|
1257
1345
|
if skipUnreachable is None:
|
|
1258
1346
|
skipUnreachable = True
|
|
1259
1347
|
if skipUnreachable:
|
|
1260
|
-
unavailableHosts =
|
|
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 =
|
|
1354
|
+
unavailableHosts = __gloablUnavailableHosts
|
|
1267
1355
|
else:
|
|
1268
1356
|
unavailableHosts = set()
|
|
1269
1357
|
skipUnreachable = True
|
|
1270
|
-
global
|
|
1271
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1424
|
+
if not __global_suppress_printout:
|
|
1337
1425
|
print(f"Running command: {command} on host: {host}")
|
|
1338
|
-
if not
|
|
1339
|
-
if not no_start: processRunOnHosts(timeout, password, max_connections, hosts, returnUnfinished,
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
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='
|
|
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:
|
|
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: {
|
|
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","--
|
|
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","--
|
|
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("-
|
|
1434
|
-
parser.add_argument("-
|
|
1435
|
-
parser.add_argument(
|
|
1436
|
-
parser.add_argument("-V","--version", action='version', version=f'%(prog)s {version} {("with sshpass " if
|
|
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
|
-
|
|
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? (
|
|
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
|
-
|
|
1618
|
+
__ipmiiInterfaceIPPrefix = args.ipmi_interface_ip_prefix
|
|
1460
1619
|
|
|
1461
|
-
if not args.greppable and not args.json and not args.
|
|
1462
|
-
|
|
1620
|
+
if not args.greppable and not args.json and not args.no_output:
|
|
1621
|
+
__global_suppress_printout = False
|
|
1463
1622
|
|
|
1464
|
-
if not
|
|
1623
|
+
if not __global_suppress_printout:
|
|
1465
1624
|
print('> ' + getStrCommand(args.hosts,args.commands,oneonone=args.oneonone,timeout=args.timeout,password=args.password,
|
|
1466
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
1493
|
-
|
|
1651
|
+
__mainReturnCode += 1
|
|
1652
|
+
__failedHosts.add(host.name)
|
|
1494
1653
|
else:
|
|
1495
1654
|
succeededHosts.add(host.name)
|
|
1496
|
-
succeededHosts -=
|
|
1655
|
+
succeededHosts -= __failedHosts
|
|
1497
1656
|
# sort the failed hosts and succeeded hosts
|
|
1498
|
-
|
|
1657
|
+
__failedHosts = sorted(__failedHosts)
|
|
1499
1658
|
succeededHosts = sorted(succeededHosts)
|
|
1500
|
-
if
|
|
1501
|
-
if not
|
|
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={
|
|
1504
|
-
if not
|
|
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
|
|
1665
|
+
if not __global_suppress_printout: print('Complete. All hosts returned 0.')
|
|
1507
1666
|
|
|
1508
|
-
if args.success_hosts and not
|
|
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
|
|
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(
|
|
1517
|
-
|
|
1518
|
-
|
|
1675
|
+
sys.exit(__mainReturnCode)
|
|
1519
1676
|
|
|
1520
1677
|
if __name__ == "__main__":
|
|
1521
1678
|
main()
|
multiSSH3-4.76.dist-info/RECORD
DELETED
|
@@ -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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|