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