multiSSH3 5.93__tar.gz → 5.95__tar.gz
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.93 → multissh3-5.95}/PKG-INFO +1 -1
- {multissh3-5.93 → multissh3-5.95}/multiSSH3.egg-info/PKG-INFO +1 -1
- {multissh3-5.93 → multissh3-5.95}/multiSSH3.py +207 -97
- {multissh3-5.93 → multissh3-5.95}/README.md +0 -0
- {multissh3-5.93 → multissh3-5.95}/multiSSH3.egg-info/SOURCES.txt +0 -0
- {multissh3-5.93 → multissh3-5.95}/multiSSH3.egg-info/dependency_links.txt +0 -0
- {multissh3-5.93 → multissh3-5.95}/multiSSH3.egg-info/entry_points.txt +0 -0
- {multissh3-5.93 → multissh3-5.95}/multiSSH3.egg-info/requires.txt +0 -0
- {multissh3-5.93 → multissh3-5.95}/multiSSH3.egg-info/top_level.txt +0 -0
- {multissh3-5.93 → multissh3-5.95}/setup.cfg +0 -0
- {multissh3-5.93 → multissh3-5.95}/setup.py +0 -0
- {multissh3-5.93 → multissh3-5.95}/test/test.py +0 -0
- {multissh3-5.93 → multissh3-5.95}/test/testCurses.py +0 -0
- {multissh3-5.93 → multissh3-5.95}/test/testCursesOld.py +0 -0
- {multissh3-5.93 → multissh3-5.95}/test/testPerfCompact.py +0 -0
- {multissh3-5.93 → multissh3-5.95}/test/testPerfExpand.py +0 -0
|
@@ -30,7 +30,7 @@ import threading
|
|
|
30
30
|
import time
|
|
31
31
|
import typing
|
|
32
32
|
import uuid
|
|
33
|
-
from collections import Counter, deque, defaultdict
|
|
33
|
+
from collections import Counter, deque, defaultdict
|
|
34
34
|
from itertools import count, product
|
|
35
35
|
|
|
36
36
|
__curses_available = False
|
|
@@ -84,10 +84,10 @@ except Exception:
|
|
|
84
84
|
print('Warning: functools.lru_cache is not available, multiSSH3 will run slower without cache.',file=sys.stderr)
|
|
85
85
|
def cache_decorator(func):
|
|
86
86
|
return func
|
|
87
|
-
version = '5.
|
|
87
|
+
version = '5.95'
|
|
88
88
|
VERSION = version
|
|
89
89
|
__version__ = version
|
|
90
|
-
COMMIT_DATE = '2025-10-
|
|
90
|
+
COMMIT_DATE = '2025-10-21'
|
|
91
91
|
|
|
92
92
|
CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
|
|
93
93
|
'~/multiSSH3.config.json',
|
|
@@ -99,6 +99,7 @@ CONFIG_FILE_CHAIN = ['./multiSSH3.config.json',
|
|
|
99
99
|
ERRORS = []
|
|
100
100
|
|
|
101
101
|
# TODO: Add terminal TUI
|
|
102
|
+
# TODO: Change -fs behavior
|
|
102
103
|
|
|
103
104
|
#%% ------------ Pre Helper Functions ----------------
|
|
104
105
|
def eprint(*args, **kwargs):
|
|
@@ -345,7 +346,16 @@ DEFAULT_ERROR_ONLY = False
|
|
|
345
346
|
DEFAULT_NO_OUTPUT = False
|
|
346
347
|
DEFAULT_RETURN_ZERO = False
|
|
347
348
|
DEFAULT_NO_ENV = False
|
|
348
|
-
DEFAULT_ENV_FILE = '
|
|
349
|
+
DEFAULT_ENV_FILE = ''
|
|
350
|
+
DEFAULT_ENV_FILES = ['/etc/profile.d/hosts.sh',
|
|
351
|
+
'~/.bashrc',
|
|
352
|
+
'~/.zshrc',
|
|
353
|
+
'~/host.env',
|
|
354
|
+
'~/hosts.env',
|
|
355
|
+
'.env',
|
|
356
|
+
'host.env',
|
|
357
|
+
'hosts.env',
|
|
358
|
+
]
|
|
349
359
|
DEFAULT_NO_HISTORY = False
|
|
350
360
|
DEFAULT_HISTORY_FILE = '~/.mssh_history'
|
|
351
361
|
DEFAULT_MAX_CONNECTIONS = 4 * os.cpu_count()
|
|
@@ -355,7 +365,7 @@ DEFAULT_GREPPABLE_MODE = False
|
|
|
355
365
|
DEFAULT_SKIP_UNREACHABLE = True
|
|
356
366
|
DEFAULT_SKIP_HOSTS = ''
|
|
357
367
|
DEFAULT_ENCODING = 'utf-8'
|
|
358
|
-
DEFAULT_DIFF_DISPLAY_THRESHOLD = 0.
|
|
368
|
+
DEFAULT_DIFF_DISPLAY_THRESHOLD = 0.6
|
|
359
369
|
SSH_STRICT_HOST_KEY_CHECKING = False
|
|
360
370
|
FORCE_TRUECOLOR = False
|
|
361
371
|
ERROR_MESSAGES_TO_IGNORE = [
|
|
@@ -386,6 +396,9 @@ if __ERROR_MESSAGES_TO_IGNORE_REGEX:
|
|
|
386
396
|
__ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile(__ERROR_MESSAGES_TO_IGNORE_REGEX)
|
|
387
397
|
else:
|
|
388
398
|
__ERROR_MESSAGES_TO_IGNORE_REGEX = re.compile('|'.join(ERROR_MESSAGES_TO_IGNORE))
|
|
399
|
+
if DEFAULT_ENV_FILE:
|
|
400
|
+
if DEFAULT_ENV_FILE not in DEFAULT_ENV_FILES:
|
|
401
|
+
DEFAULT_ENV_FILES.append(DEFAULT_ENV_FILE)
|
|
389
402
|
|
|
390
403
|
#%% Load mssh Functional Global Variables
|
|
391
404
|
__global_suppress_printout = False
|
|
@@ -393,7 +406,7 @@ __mainReturnCode = 0
|
|
|
393
406
|
__failedHosts = set()
|
|
394
407
|
__wildCharacters = ['*','?','x']
|
|
395
408
|
_no_env = DEFAULT_NO_ENV
|
|
396
|
-
|
|
409
|
+
_env_files = DEFAULT_ENV_FILES
|
|
397
410
|
__globalUnavailableHosts = dict()
|
|
398
411
|
__ipmiiInterfaceIPPrefix = DEFAULT_IPMI_INTERFACE_IP_PREFIX
|
|
399
412
|
__keyPressesIn = [[]]
|
|
@@ -475,35 +488,65 @@ def find_ssh_key_file(searchPath = DEDAULT_SSH_KEY_SEARCH_PATH):
|
|
|
475
488
|
return None
|
|
476
489
|
|
|
477
490
|
@cache_decorator
|
|
478
|
-
def readEnvFromFile(
|
|
491
|
+
def readEnvFromFile():
|
|
479
492
|
'''
|
|
480
493
|
Read the environment variables from env_file
|
|
481
494
|
Returns:
|
|
482
495
|
dict: A dictionary of environment variables
|
|
483
496
|
'''
|
|
484
|
-
global
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
envf =
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
if line.startswith('#') or not line
|
|
497
|
+
global _env_files
|
|
498
|
+
global _no_env
|
|
499
|
+
envfs = _env_files if _env_files else DEFAULT_ENV_FILES
|
|
500
|
+
translator = str.maketrans('&|"', ';;\'')
|
|
501
|
+
replacement_re = re.compile(r'\$(?:[A-Za-z_]\w*|\{[A-Za-z_]\w*\})')
|
|
502
|
+
environemnt = {}
|
|
503
|
+
scrubCounter = 0
|
|
504
|
+
for envf in envfs:
|
|
505
|
+
envf = os.path.expanduser(os.path.expandvars(envf))
|
|
506
|
+
if os.path.exists(envf):
|
|
507
|
+
with open(envf,'r') as f:
|
|
508
|
+
lines = f.readlines()
|
|
509
|
+
for line in lines:
|
|
510
|
+
line = line.strip()
|
|
511
|
+
if not line or line.startswith('#') or '=' not in line:
|
|
499
512
|
continue
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
513
|
+
line = line.translate(translator)
|
|
514
|
+
commands = re.split(r";(?=(?:[^']*'[^']*')*[^']*$)", line)
|
|
515
|
+
for command in commands:
|
|
516
|
+
if not command or command.startswith('#') or '=' not in command or command.startswith('alias '):
|
|
517
|
+
continue
|
|
518
|
+
fields = re.split(r" (?=(?:[^']*'[^']*')*[^']*$)", command)
|
|
519
|
+
for field in fields:
|
|
520
|
+
try:
|
|
521
|
+
if field.startswith('export '):
|
|
522
|
+
field = field.replace('export ', '', 1).strip()
|
|
523
|
+
if not field or field.startswith('#') or '=' not in field:
|
|
524
|
+
continue
|
|
525
|
+
key, _, values = field.partition('=')
|
|
526
|
+
key = key.strip().strip("'")
|
|
527
|
+
values = values.strip().strip("'")
|
|
528
|
+
if '$' in values:
|
|
529
|
+
scrubCounter += 16
|
|
530
|
+
if key and values and key != values:
|
|
531
|
+
environemnt[key] = values
|
|
532
|
+
except Exception:
|
|
533
|
+
continue
|
|
534
|
+
while scrubCounter:
|
|
535
|
+
scrubCounter -= 1
|
|
536
|
+
found = False
|
|
537
|
+
for key, value in environemnt.items():
|
|
538
|
+
if '$' in value:
|
|
539
|
+
for match in replacement_re.findall(value):
|
|
540
|
+
ref_key = match.strip('${}')
|
|
541
|
+
ref_value = environemnt.get(ref_key) if ref_key != key else None
|
|
542
|
+
if not ref_value and not _no_env:
|
|
543
|
+
ref_value = os.environ.get(ref_key)
|
|
544
|
+
if ref_value:
|
|
545
|
+
environemnt[key] = value.replace(match, ref_value)
|
|
546
|
+
found = True
|
|
547
|
+
if not found:
|
|
548
|
+
break
|
|
549
|
+
return environemnt
|
|
507
550
|
|
|
508
551
|
def replace_magic_strings(string,keys,value,case_sensitive=False):
|
|
509
552
|
'''
|
|
@@ -641,28 +684,25 @@ class OrderedMultiSet(deque):
|
|
|
641
684
|
self._counter = Counter()
|
|
642
685
|
if iterable is not None:
|
|
643
686
|
self.extend(iterable)
|
|
644
|
-
def __decrease_count(self, item):
|
|
645
|
-
"""Decrease count of item in counter."""
|
|
646
|
-
self._counter[item] -= 1
|
|
647
|
-
if self._counter[item] == 0:
|
|
648
|
-
del self._counter[item]
|
|
649
687
|
def append(self, item):
|
|
650
688
|
"""Add item to the right end. O(1)."""
|
|
651
689
|
if len(self) == self.maxlen:
|
|
652
|
-
self.
|
|
690
|
+
self._counter -= Counter([self[0]])
|
|
691
|
+
# self._counter[self[0]] -= 1
|
|
692
|
+
# self._counter += Counter()
|
|
653
693
|
super().append(item)
|
|
654
694
|
self._counter[item] += 1
|
|
655
695
|
def appendleft(self, item):
|
|
656
696
|
"""Add item to the left end. O(1)."""
|
|
657
697
|
if len(self) == self.maxlen:
|
|
658
|
-
self.
|
|
698
|
+
self._counter -= Counter([self[-1]])
|
|
659
699
|
super().appendleft(item)
|
|
660
700
|
self._counter[item] += 1
|
|
661
701
|
def pop(self):
|
|
662
702
|
"""Remove and return item from right end. O(1)."""
|
|
663
703
|
try:
|
|
664
704
|
item = super().pop()
|
|
665
|
-
self.
|
|
705
|
+
self._counter -= Counter([item])
|
|
666
706
|
return item
|
|
667
707
|
except IndexError:
|
|
668
708
|
return None
|
|
@@ -670,7 +710,7 @@ class OrderedMultiSet(deque):
|
|
|
670
710
|
"""Remove and return item from left end. O(1)."""
|
|
671
711
|
try:
|
|
672
712
|
item = super().popleft()
|
|
673
|
-
self.
|
|
713
|
+
self._counter -= Counter([item])
|
|
674
714
|
return item
|
|
675
715
|
except IndexError:
|
|
676
716
|
return None
|
|
@@ -679,7 +719,7 @@ class OrderedMultiSet(deque):
|
|
|
679
719
|
removed = None
|
|
680
720
|
if len(self) == self.maxlen:
|
|
681
721
|
removed = self[0] # Item that will be removed
|
|
682
|
-
self.
|
|
722
|
+
self._counter -= Counter([removed])
|
|
683
723
|
super().append(item)
|
|
684
724
|
self._counter[item] += 1
|
|
685
725
|
return removed
|
|
@@ -688,7 +728,7 @@ class OrderedMultiSet(deque):
|
|
|
688
728
|
removed = None
|
|
689
729
|
if len(self) == self.maxlen:
|
|
690
730
|
removed = self[-1] # Item that will be removed
|
|
691
|
-
self.
|
|
731
|
+
self._counter -= Counter([removed])
|
|
692
732
|
super().appendleft(item)
|
|
693
733
|
self._counter[item] += 1
|
|
694
734
|
return removed
|
|
@@ -700,7 +740,7 @@ class OrderedMultiSet(deque):
|
|
|
700
740
|
if value not in self._counter:
|
|
701
741
|
return None
|
|
702
742
|
super().remove(value)
|
|
703
|
-
self.
|
|
743
|
+
self._counter -= Counter([value])
|
|
704
744
|
def clear(self):
|
|
705
745
|
"""Remove all items. O(1)."""
|
|
706
746
|
super().clear()
|
|
@@ -721,24 +761,61 @@ class OrderedMultiSet(deque):
|
|
|
721
761
|
super().extend(iterable)
|
|
722
762
|
self._counter.update(iterable)
|
|
723
763
|
else:
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
for _ in range(num_to_remove):
|
|
727
|
-
self.__decrease_count(super().popleft())
|
|
764
|
+
num_to_keep = self.maxlen - len(iterable)
|
|
765
|
+
self.truncateright(num_to_keep)
|
|
728
766
|
super().extend(iterable)
|
|
729
767
|
self._counter.update(iterable)
|
|
730
768
|
except TypeError:
|
|
731
769
|
return self.extend(list(iterable))
|
|
732
770
|
def extendleft(self, iterable):
|
|
733
771
|
"""Extend left side by appending elements from iterable. O(k)."""
|
|
734
|
-
|
|
735
|
-
|
|
772
|
+
# if maxlen is set, and the new length exceeds maxlen, we clear then efficiently extendleft
|
|
773
|
+
try:
|
|
774
|
+
if not self.maxlen or len(self) + len(iterable) <= self.maxlen:
|
|
775
|
+
super().extendleft(iterable)
|
|
776
|
+
self._counter.update(iterable)
|
|
777
|
+
elif len(iterable) >= self.maxlen:
|
|
778
|
+
self.clear()
|
|
779
|
+
if isinstance(iterable, (list, tuple)):
|
|
780
|
+
iterable = iterable[:self.maxlen]
|
|
781
|
+
else:
|
|
782
|
+
iterable = itertools.islice(iterable, 0, self.maxlen)
|
|
783
|
+
super().extendleft(iterable)
|
|
784
|
+
self._counter.update(iterable)
|
|
785
|
+
else:
|
|
786
|
+
num_to_keep = self.maxlen - len(iterable)
|
|
787
|
+
self.truncate(num_to_keep)
|
|
788
|
+
super().extendleft(iterable)
|
|
789
|
+
self._counter.update(iterable)
|
|
790
|
+
except TypeError:
|
|
791
|
+
return self.extendleft(list(iterable))
|
|
792
|
+
def update(self, iterable):
|
|
793
|
+
"""Extend deque by appending elements from iterable. Alias for extend. O(k)."""
|
|
794
|
+
return self.extend(iterable)
|
|
795
|
+
def updateleft(self, iterable):
|
|
796
|
+
"""Extend left side by appending elements from iterable. Alias for extendleft. O(k)."""
|
|
797
|
+
return self.extendleft(iterable)
|
|
798
|
+
def truncate(self, n):
|
|
799
|
+
"""Truncate to keep left n items. O(n)."""
|
|
800
|
+
kept = list(itertools.islice(self, n))
|
|
801
|
+
dropped = Counter(itertools.islice(self, n, None))
|
|
802
|
+
super().clear()
|
|
803
|
+
super().extend(kept)
|
|
804
|
+
self._counter -= dropped
|
|
805
|
+
def truncateright(self, n):
|
|
806
|
+
"""Truncate to keep right n items. O(n)."""
|
|
807
|
+
kept = list(itertools.islice(self, len(self) - n, None))
|
|
808
|
+
dropped = Counter(itertools.islice(self, 0, len(self) - n))
|
|
809
|
+
super().clear()
|
|
810
|
+
super().extend(kept)
|
|
811
|
+
self._counter -= dropped
|
|
736
812
|
def rotate(self, n=1):
|
|
737
813
|
"""Rotate deque n steps to the right. O(k) where k = min(n, len)."""
|
|
738
814
|
super().rotate(n)
|
|
739
815
|
def __contains__(self, item):
|
|
740
816
|
"""Check if item exists in deque. O(1) average."""
|
|
741
|
-
return item in self._counter
|
|
817
|
+
# return item in self._counter
|
|
818
|
+
return super().__contains__(item)
|
|
742
819
|
def count(self, item):
|
|
743
820
|
"""Return number of occurrences of item. O(1)."""
|
|
744
821
|
return self._counter[item]
|
|
@@ -746,14 +823,14 @@ class OrderedMultiSet(deque):
|
|
|
746
823
|
"""Set item at index. O(1) for access, O(1) for counter update."""
|
|
747
824
|
old_value = self[index]
|
|
748
825
|
super().__setitem__(index, value)
|
|
749
|
-
self.
|
|
826
|
+
self._counter -= Counter([old_value])
|
|
750
827
|
self._counter[value] += 1
|
|
751
828
|
return old_value
|
|
752
829
|
def __delitem__(self, index):
|
|
753
830
|
"""Delete item at index. O(n) for deletion, O(1) for counter update."""
|
|
754
831
|
value = self[index]
|
|
755
832
|
super().__delitem__(index)
|
|
756
|
-
self.
|
|
833
|
+
self._counter -= Counter([value])
|
|
757
834
|
return value
|
|
758
835
|
def insert(self, index, value):
|
|
759
836
|
"""Insert value at index. O(n) for insertion, O(1) for counter update."""
|
|
@@ -787,6 +864,28 @@ class OrderedMultiSet(deque):
|
|
|
787
864
|
return self[-1]
|
|
788
865
|
except IndexError:
|
|
789
866
|
return None
|
|
867
|
+
def __iadd__(self, value):
|
|
868
|
+
return self.extend(value)
|
|
869
|
+
def __add__(self, value):
|
|
870
|
+
new_deque = self.copy()
|
|
871
|
+
new_deque.extend(value)
|
|
872
|
+
return new_deque
|
|
873
|
+
def __mul__(self, value):
|
|
874
|
+
new_deque = OrderedMultiSet(maxlen=self.maxlen)
|
|
875
|
+
for _ in range(value):
|
|
876
|
+
new_deque.extend(self)
|
|
877
|
+
return new_deque
|
|
878
|
+
def __imul__(self, value):
|
|
879
|
+
if value <= 0:
|
|
880
|
+
self.clear()
|
|
881
|
+
return self
|
|
882
|
+
for _ in range(value - 1):
|
|
883
|
+
self.extend(self)
|
|
884
|
+
return self
|
|
885
|
+
def __eq__(self, value):
|
|
886
|
+
if isinstance(value, OrderedMultiSet):
|
|
887
|
+
return self._counter == value._counter
|
|
888
|
+
return super().__eq__(value)
|
|
790
889
|
|
|
791
890
|
def get_terminal_size():
|
|
792
891
|
'''
|
|
@@ -2687,8 +2786,9 @@ def can_merge(line_bag1, line_bag2, threshold):
|
|
|
2687
2786
|
return len(line_bag1.intersection(line_bag2)) >= min(len(line_bag1),len(line_bag2)) * threshold
|
|
2688
2787
|
|
|
2689
2788
|
def mergeOutput(merging_hostnames,outputs_by_hostname,output,diff_display_threshold,line_length):
|
|
2690
|
-
indexes = {hostname: 0 for hostname in merging_hostnames}
|
|
2691
|
-
|
|
2789
|
+
#indexes = {hostname: 0 for hostname in merging_hostnames}
|
|
2790
|
+
indexes = Counter({hostname: 0 for hostname in merging_hostnames})
|
|
2791
|
+
working_index_keys = set(merging_hostnames)
|
|
2692
2792
|
previousBuddies = set()
|
|
2693
2793
|
hostnameWrapper = textwrap.TextWrapper(width=line_length - 1, tabsize=4, replace_whitespace=False, drop_whitespace=False, break_on_hyphens=False,initial_indent='├─ ', subsequent_indent='│- ')
|
|
2694
2794
|
hostnameWrapper.wordsep_simple_re = re.compile(r'([,]+)')
|
|
@@ -2696,26 +2796,41 @@ def mergeOutput(merging_hostnames,outputs_by_hostname,output,diff_display_thresh
|
|
|
2696
2796
|
def get_multiset_index_for_hostname(hostname):
|
|
2697
2797
|
index = indexes[hostname]
|
|
2698
2798
|
tracking_index = min(index + diff_display_item_count,len(outputs_by_hostname[hostname]))
|
|
2699
|
-
|
|
2799
|
+
tracking_iter = itertools.islice(outputs_by_hostname[hostname], tracking_index)
|
|
2800
|
+
return [deque(outputs_by_hostname[hostname][index:tracking_index],maxlen=diff_display_item_count),tracking_iter]
|
|
2700
2801
|
# futuresChainMap = ChainMap()
|
|
2701
|
-
class futureDict(UserDict):
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2802
|
+
# class futureDict(UserDict):
|
|
2803
|
+
# def __missing__(self, key):
|
|
2804
|
+
# value = get_multiset_index_for_hostname(key)
|
|
2805
|
+
# self[key] = value
|
|
2806
|
+
# # futuresChainMap.maps.append(value[0]._counter)
|
|
2807
|
+
# return value
|
|
2808
|
+
# # def initializeHostnames(self, hostnames):
|
|
2809
|
+
# # entries = {hostname: get_multiset_index_for_hostname(hostname) for hostname in hostnames}
|
|
2810
|
+
# # self.update(entries)
|
|
2811
|
+
# # futuresChainMap.maps.extend(entry[0]._counter for entry in entries.values())
|
|
2812
|
+
def advance(dict,key):
|
|
2813
|
+
try:
|
|
2814
|
+
value = dict[key]
|
|
2815
|
+
value[0].append(next(value[1]))
|
|
2816
|
+
except StopIteration:
|
|
2817
|
+
try:
|
|
2818
|
+
value[0].popleft()
|
|
2819
|
+
except IndexError:
|
|
2820
|
+
pass
|
|
2821
|
+
except KeyError:
|
|
2822
|
+
pass
|
|
2823
|
+
# futures = futureDict()
|
|
2824
|
+
# for hostname in merging_hostnames:
|
|
2825
|
+
# futures[hostname] # ensure it's initialized
|
|
2826
|
+
futures = {hostname: get_multiset_index_for_hostname(hostname) for hostname in merging_hostnames}
|
|
2712
2827
|
currentLines = defaultdict(set)
|
|
2713
2828
|
for hostname in merging_hostnames:
|
|
2714
2829
|
currentLines[outputs_by_hostname[hostname][0]].add(hostname)
|
|
2715
2830
|
while indexes:
|
|
2716
2831
|
defer = False
|
|
2717
2832
|
# sorted_working_hostnames = sorted(working_index_keys, key=lambda hn: indexes[hn])
|
|
2718
|
-
golden_hostname = min(working_index_keys, key=
|
|
2833
|
+
golden_hostname = min(working_index_keys, key=indexes.get)
|
|
2719
2834
|
golden_index = indexes[golden_hostname]
|
|
2720
2835
|
lineToAdd = outputs_by_hostname[golden_hostname][golden_index]
|
|
2721
2836
|
# for hostname, index in sorted_working_indexes[1:]:
|
|
@@ -2735,8 +2850,8 @@ def mergeOutput(merging_hostnames,outputs_by_hostname,output,diff_display_thresh
|
|
|
2735
2850
|
# if golden_hostname in futures:
|
|
2736
2851
|
# thisCounter = futures[golden_hostname][0]._counter
|
|
2737
2852
|
# futuresChainMap.maps.remove(thisCounter)
|
|
2738
|
-
for hostname in working_index_keys - buddy - set(futures.keys()):
|
|
2739
|
-
|
|
2853
|
+
# for hostname in working_index_keys - buddy - set(futures.keys()):
|
|
2854
|
+
# futures[hostname] # ensure it's initialized
|
|
2740
2855
|
# futures.initializeHostnames(working_index_keys - buddy - futures.keys())
|
|
2741
2856
|
if any(lineToAdd in futures[hostname][0] for hostname in working_index_keys - buddy):
|
|
2742
2857
|
defer = True
|
|
@@ -2756,11 +2871,12 @@ def mergeOutput(merging_hostnames,outputs_by_hostname,output,diff_display_thresh
|
|
|
2756
2871
|
currentLines[lineToAdd].difference_update(buddy)
|
|
2757
2872
|
if not currentLines[lineToAdd]:
|
|
2758
2873
|
del currentLines[lineToAdd]
|
|
2874
|
+
indexes.update(buddy)
|
|
2759
2875
|
for hostname in buddy:
|
|
2760
2876
|
# currentLines[lineToAdd].remove(hostname)
|
|
2761
2877
|
# if not currentLines[lineToAdd]:
|
|
2762
2878
|
# del currentLines[lineToAdd]
|
|
2763
|
-
indexes[hostname] += 1
|
|
2879
|
+
# indexes[hostname] += 1
|
|
2764
2880
|
try:
|
|
2765
2881
|
currentLines[outputs_by_hostname[hostname][indexes[hostname]]].add(hostname)
|
|
2766
2882
|
except IndexError:
|
|
@@ -2770,15 +2886,7 @@ def mergeOutput(merging_hostnames,outputs_by_hostname,output,diff_display_thresh
|
|
|
2770
2886
|
# futuresChainMap.maps.remove(future[0]._counter)
|
|
2771
2887
|
continue
|
|
2772
2888
|
#advance futures
|
|
2773
|
-
|
|
2774
|
-
futures[hostname][1] += 1
|
|
2775
|
-
tracking_multiset, tracking_index = futures[hostname]
|
|
2776
|
-
if tracking_index < len(outputs_by_hostname[hostname]):
|
|
2777
|
-
line = outputs_by_hostname[hostname][tracking_index]
|
|
2778
|
-
tracking_multiset.append(line)
|
|
2779
|
-
else:
|
|
2780
|
-
tracking_multiset.popleft()
|
|
2781
|
-
#futures[hostname] = (tracking_multiset, tracking_index)
|
|
2889
|
+
advance(futures, hostname)
|
|
2782
2890
|
working_index_keys = set(indexes.keys())
|
|
2783
2891
|
|
|
2784
2892
|
def mergeOutputs(outputs_by_hostname, merge_groups, remaining_hostnames, diff_display_threshold, line_length):
|
|
@@ -2830,16 +2938,17 @@ def get_host_raw_output(hosts, terminal_width):
|
|
|
2830
2938
|
prevLine = host.command
|
|
2831
2939
|
if host.stdout:
|
|
2832
2940
|
hostPrintOut.append('│▓ STDOUT:')
|
|
2833
|
-
for line in host.stdout:
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2941
|
+
# for line in host.stdout:
|
|
2942
|
+
# if len(line) < terminal_width - 2:
|
|
2943
|
+
# hostPrintOut.append(f"│ {line}")
|
|
2944
|
+
# else:
|
|
2945
|
+
# hostPrintOut.extend(text_wrapper.wrap(line))
|
|
2946
|
+
hostPrintOut.extend(f"│ {line}" for line in host.stdout)
|
|
2838
2947
|
# hostPrintOut.extend(text_wrapper.wrap(line) for line in host.stdout)
|
|
2839
2948
|
lineBag.add((prevLine,1))
|
|
2840
2949
|
lineBag.add((1,host.stdout[0]))
|
|
2841
2950
|
if len(host.stdout) > 1:
|
|
2842
|
-
lineBag.update(
|
|
2951
|
+
lineBag.update(itertools.pairwise(host.stdout))
|
|
2843
2952
|
lineBag.update(host.stdout)
|
|
2844
2953
|
prevLine = host.stdout[-1]
|
|
2845
2954
|
if host.stderr:
|
|
@@ -2851,16 +2960,17 @@ def get_host_raw_output(hosts, terminal_width):
|
|
|
2851
2960
|
host.stderr[-1] = 'Cannot find host!'
|
|
2852
2961
|
if host.stderr:
|
|
2853
2962
|
hostPrintOut.append('│▒ STDERR:')
|
|
2854
|
-
for line in host.stderr:
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2963
|
+
# for line in host.stderr:
|
|
2964
|
+
# if len(line) < terminal_width - 2:
|
|
2965
|
+
# hostPrintOut.append(f"│ {line}")
|
|
2966
|
+
# else:
|
|
2967
|
+
# hostPrintOut.extend(text_wrapper.wrap(line))
|
|
2968
|
+
hostPrintOut.extend(f"│ {line}" for line in host.stderr)
|
|
2859
2969
|
lineBag.add((prevLine,2))
|
|
2860
2970
|
lineBag.add((2,host.stderr[0]))
|
|
2861
2971
|
lineBag.update(host.stderr)
|
|
2862
2972
|
if len(host.stderr) > 1:
|
|
2863
|
-
lineBag.update(
|
|
2973
|
+
lineBag.update(itertools.pairwise(host.stderr))
|
|
2864
2974
|
prevLine = host.stderr[-1]
|
|
2865
2975
|
hostPrintOut.append(f"│░ RETURN CODE: {host.returncode}")
|
|
2866
2976
|
lineBag.add((prevLine,f"{host.returncode}"))
|
|
@@ -3099,7 +3209,7 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
|
|
|
3099
3209
|
no_env=DEFAULT_NO_ENV,greppable=DEFAULT_GREPPABLE_MODE,skip_hosts = DEFAULT_SKIP_HOSTS,
|
|
3100
3210
|
file_sync = False, error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
|
|
3101
3211
|
copy_id = False, unavailable_host_expiry = DEFAULT_UNAVAILABLE_HOST_EXPIRY, no_history = DEFAULT_NO_HISTORY,
|
|
3102
|
-
history_file = DEFAULT_HISTORY_FILE, env_file =
|
|
3212
|
+
history_file = DEFAULT_HISTORY_FILE, env_file = DEFAULT_ENV_FILES,
|
|
3103
3213
|
repeat = DEFAULT_REPEAT,interval = DEFAULT_INTERVAL,
|
|
3104
3214
|
shortend = False) -> str:
|
|
3105
3215
|
argsList = []
|
|
@@ -3143,8 +3253,8 @@ def __formCommandArgStr(oneonone = DEFAULT_ONE_ON_ONE, timeout = DEFAULT_TIMEOUT
|
|
|
3143
3253
|
argsList.append(f'--unavailable_host_expiry={unavailable_host_expiry}' if not shortend else f'-uhe={unavailable_host_expiry}')
|
|
3144
3254
|
if no_env:
|
|
3145
3255
|
argsList.append('--no_env')
|
|
3146
|
-
if env_file and env_file !=
|
|
3147
|
-
argsList.
|
|
3256
|
+
if env_file and env_file != DEFAULT_ENV_FILES:
|
|
3257
|
+
argsList.extend([f'--env_file="{ef}"' for ef in env_file] if not shortend else [f'-ef="{ef}"' for ef in env_file])
|
|
3148
3258
|
if no_history:
|
|
3149
3259
|
argsList.append('--no_history' if not shortend else '-nh')
|
|
3150
3260
|
if history_file and history_file != DEFAULT_HISTORY_FILE:
|
|
@@ -3167,7 +3277,7 @@ def getStrCommand(hosts = DEFAULT_HOSTS,commands = None,oneonone = DEFAULT_ONE_O
|
|
|
3167
3277
|
skip_hosts = DEFAULT_SKIP_HOSTS, curses_min_char_len = DEFAULT_CURSES_MINIMUM_CHAR_LEN, curses_min_line_len = DEFAULT_CURSES_MINIMUM_LINE_LEN,
|
|
3168
3278
|
single_window = DEFAULT_SINGLE_WINDOW,file_sync = False,error_only = DEFAULT_ERROR_ONLY, identity_file = DEFAULT_IDENTITY_FILE,
|
|
3169
3279
|
copy_id = False, unavailable_host_expiry = DEFAULT_UNAVAILABLE_HOST_EXPIRY,no_history = DEFAULT_NO_HISTORY,
|
|
3170
|
-
history_file = DEFAULT_HISTORY_FILE, env_file =
|
|
3280
|
+
history_file = DEFAULT_HISTORY_FILE, env_file = DEFAULT_ENV_FILES,
|
|
3171
3281
|
repeat = DEFAULT_REPEAT,interval = DEFAULT_INTERVAL,
|
|
3172
3282
|
shortend = False,tabSeperated = False):
|
|
3173
3283
|
_ = called
|
|
@@ -3579,7 +3689,7 @@ def generate_default_config(args):
|
|
|
3579
3689
|
'DEFAULT_NO_OUTPUT': args.no_output,
|
|
3580
3690
|
'DEFAULT_RETURN_ZERO': args.return_zero,
|
|
3581
3691
|
'DEFAULT_NO_ENV': args.no_env,
|
|
3582
|
-
'
|
|
3692
|
+
'DEFAULT_ENV_FILES': args.env_file,
|
|
3583
3693
|
'DEFAULT_NO_HISTORY': args.no_history,
|
|
3584
3694
|
'DEFAULT_HISTORY_FILE': args.history_file,
|
|
3585
3695
|
'DEFAULT_MAX_CONNECTIONS': args.max_connections if args.max_connections != 4 * os.cpu_count() else None,
|
|
@@ -3668,7 +3778,7 @@ def get_parser():
|
|
|
3668
3778
|
parser.add_argument('-Q',"-no","--no_output", action='store_true', help=f"Do not print the output. (default: {DEFAULT_NO_OUTPUT})", default=DEFAULT_NO_OUTPUT)
|
|
3669
3779
|
parser.add_argument('-Z','-rz','--return_zero', action='store_true', help=f"Return 0 even if there are errors. (default: {DEFAULT_RETURN_ZERO})", default=DEFAULT_RETURN_ZERO)
|
|
3670
3780
|
parser.add_argument('-C','--no_env', action='store_true', help=f'Do not load the command line environment variables. (default: {DEFAULT_NO_ENV})', default=DEFAULT_NO_ENV)
|
|
3671
|
-
parser.add_argument("--env_file",
|
|
3781
|
+
parser.add_argument("--env_file", action='append', help=f"The files to load the mssh file based environment variables from. Can specify multiple. Load first to last. ( Still work with --no_env ) (default: {DEFAULT_ENV_FILES})", default=DEFAULT_ENV_FILES)
|
|
3672
3782
|
parser.add_argument("-m","--max_connections", type=int, help="Max number of connections to use (default: 4 * cpu_count)", default=DEFAULT_MAX_CONNECTIONS)
|
|
3673
3783
|
parser.add_argument("-j","--json", action='store_true', help=F"Output in json format. (default: {DEFAULT_JSON_MODE})", default=DEFAULT_JSON_MODE)
|
|
3674
3784
|
parser.add_argument('-w',"--success_hosts", action='store_true', help=f"Output the hosts that succeeded in summary as well. (default: {DEFAULT_PRINT_SUCCESS_HOSTS})", default=DEFAULT_PRINT_SUCCESS_HOSTS)
|
|
@@ -3783,7 +3893,7 @@ def process_keys(args):
|
|
|
3783
3893
|
def set_global_with_args(args):
|
|
3784
3894
|
global _emo
|
|
3785
3895
|
global __ipmiiInterfaceIPPrefix
|
|
3786
|
-
global
|
|
3896
|
+
global _env_files
|
|
3787
3897
|
global __DEBUG_MODE
|
|
3788
3898
|
global __configs_from_file
|
|
3789
3899
|
global _encoding
|
|
@@ -3794,7 +3904,7 @@ def set_global_with_args(args):
|
|
|
3794
3904
|
global FORCE_TRUECOLOR
|
|
3795
3905
|
_emo = False
|
|
3796
3906
|
__ipmiiInterfaceIPPrefix = args.ipmi_interface_ip_prefix
|
|
3797
|
-
|
|
3907
|
+
_env_files = args.env_file
|
|
3798
3908
|
__DEBUG_MODE = args.debug
|
|
3799
3909
|
_encoding = args.encoding
|
|
3800
3910
|
if args.return_zero:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|