eping 4.2__tar.gz → 4.4__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.
- {eping-4.2 → eping-4.4}/PKG-INFO +13 -5
- {eping-4.2 → eping-4.4}/README.md +12 -4
- {eping-4.2 → eping-4.4}/pyproject.toml +1 -1
- {eping-4.2 → eping-4.4}/src/eping/eping.py +263 -33
- {eping-4.2 → eping-4.4}/src/eping.egg-info/PKG-INFO +13 -5
- {eping-4.2 → eping-4.4}/LICENSE +0 -0
- {eping-4.2 → eping-4.4}/setup.cfg +0 -0
- {eping-4.2 → eping-4.4}/src/eping/__init__.py +0 -0
- {eping-4.2 → eping-4.4}/src/eping/_launch_eping.py +0 -0
- {eping-4.2 → eping-4.4}/src/eping/epinga.py +0 -0
- {eping-4.2 → eping-4.4}/src/eping.egg-info/SOURCES.txt +0 -0
- {eping-4.2 → eping-4.4}/src/eping.egg-info/dependency_links.txt +0 -0
- {eping-4.2 → eping-4.4}/src/eping.egg-info/entry_points.txt +0 -0
- {eping-4.2 → eping-4.4}/src/eping.egg-info/top_level.txt +0 -0
{eping-4.2 → eping-4.4}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eping
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.4
|
|
4
4
|
Summary: Continuous ICMP reachability monitor built on fping, with a CSV logfile analyser (epinga)
|
|
5
5
|
Author-email: Ewald Jeitler <ewald@jeitler.cc>
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Dynamic: license-file
|
|
19
19
|
|
|
20
|
-
# eping.py 4.
|
|
20
|
+
# eping.py 4.04
|
|
21
21
|
|
|
22
22
|
Continuous ICMP reachability monitor built on `fping`. Scans a host list in a loop,
|
|
23
23
|
reports each host UP/DOWN/NO-DNS, counts state changes. CLI (curses) or web GUI.
|
|
@@ -449,6 +449,7 @@ separated) · `-r` IP range (comma separated, shortened end)
|
|
|
449
449
|
| `-ssh` | `[USER@]HOST[:PORT]` | off | run fping on a jump host via ssh (see *SSH source (JUMPHOST)*) |
|
|
450
450
|
| `-sshkey` | file | agent | private key file for `-ssh` |
|
|
451
451
|
| `-sshhk` | accept-new/ask/off | accept-new | host key check for `-ssh` |
|
|
452
|
+
| `-sshnr` | — | off | with `-ssh`: do not resolve names on the jump host (use the local resolver) |
|
|
452
453
|
| `-ncf` | — | off | CSV: do not ping the CSV IP of a name without DNS record |
|
|
453
454
|
| `-up` | — | 0 | learning phase: after N rounds show only hosts seen UP (view CURRENTLY-UP; all hosts are still pinged until SET REFERENCE) |
|
|
454
455
|
| `-setref` | — | off | with `-up`: hosts seen UP become new reference list |
|
|
@@ -509,24 +510,31 @@ these rows automatically.
|
|
|
509
510
|
## SSH source (JUMPHOST)
|
|
510
511
|
|
|
511
512
|
Run fping on a jump host instead of locally (system `ssh` only). Any Linux host with a working `fping`
|
|
512
|
-
(e.g. a caretaker) will do; it resolves the names itself. Start: `-ssh [USER@]HOST[:PORT]` (+ `-sshkey FILE`, `-sshhk MODE`);
|
|
513
|
+
(e.g. a caretaker) will do; it resolves the names itself. Start: `-ssh [USER@]HOST[:PORT]` (+ `-sshkey FILE`, `-sshhk MODE`, `-sshnr`);
|
|
513
514
|
the password or key passphrase is asked on the terminal at startup. At runtime: `J` in the CLI,
|
|
514
515
|
JUMPHOST button in the web GUI (after ADV OPTIONS).
|
|
515
516
|
|
|
516
517
|

|
|
517
518
|
|
|
518
|
-
- Window: HOST, PORT, USER, PASSWORD (key passphrase), HOST KEY, KEY FILE. TEST checks ssh, login and
|
|
519
|
+
- Window: HOST, PORT, USER, PASSWORD (key passphrase), HOST KEY, NAMES, KEY FILE. TEST checks ssh, login and
|
|
519
520
|
fping on the jump host, CONNECT switches the scan source, USE LOCAL switches back. A missing local
|
|
520
521
|
`ssh` is shown in the window.
|
|
521
522
|
- HOST KEY: `accept-new` (default) - hosts seen for the first time are trusted automatically, a changed
|
|
522
523
|
key is still refused; `ask` - an unknown key is shown with its fingerprint and must be accepted with
|
|
523
524
|
TRUST KEY; `off` - no check at all (insecure).
|
|
525
|
+
- NAMES (default on): the DNS lookups eping does itself - GET NAMES, PREFER HOSTNAME / IP ADDRESS, IP ONLY -
|
|
526
|
+
run on the jump host over the same ssh connection (no second login), using `getent`, else `dig`,
|
|
527
|
+
`nslookup` or `host` there; TEST/CONNECT report which tool works. One batch per call, names are validated
|
|
528
|
+
and sent via stdin. Without a working tool, or with the option off (`-sshnr`), the local resolver is used
|
|
529
|
+
and GET NAMES is not available. The DNS caches are cleared on every source switch.
|
|
530
|
+
A name with only an AAAA record is pinged with `fping -6` there (A is preferred, as locally). Without a resolver on
|
|
531
|
+
the jump host, a name that fping reports as NO-DNS is tried with `-6` in the next round, alternating with `-4`; `-4` / `-6` force one family.
|
|
524
532
|
- The switch is applied at the start of the next round; hosts, states and counters are kept.
|
|
525
533
|
- Connection loss: results of the round are discarded, hosts keep their state, a banner is shown and
|
|
526
534
|
eping reconnects. Logged as `#COMMENT#` rows (`JUMPHOST ... OFFLINE: <reason>`, `JUMPHOST ... ONLINE
|
|
527
535
|
again`, `SOURCE SWITCH <old> -> <new>`), shown in the report's comment timeline.
|
|
528
536
|
- Log column `SOURCE`: `local` or the jump host of each row.
|
|
529
|
-
- Limits:
|
|
537
|
+
- Limits: the CSV-IP fallback is not available over ssh; at most 4 ssh processes
|
|
530
538
|
(2 on Windows); the web password transfer is plain HTTP - use `-bind 127.0.0.1` or a trusted network.
|
|
531
539
|
|
|
532
540
|
## Web GUI header
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# eping.py 4.
|
|
1
|
+
# eping.py 4.04
|
|
2
2
|
|
|
3
3
|
Continuous ICMP reachability monitor built on `fping`. Scans a host list in a loop,
|
|
4
4
|
reports each host UP/DOWN/NO-DNS, counts state changes. CLI (curses) or web GUI.
|
|
@@ -430,6 +430,7 @@ separated) · `-r` IP range (comma separated, shortened end)
|
|
|
430
430
|
| `-ssh` | `[USER@]HOST[:PORT]` | off | run fping on a jump host via ssh (see *SSH source (JUMPHOST)*) |
|
|
431
431
|
| `-sshkey` | file | agent | private key file for `-ssh` |
|
|
432
432
|
| `-sshhk` | accept-new/ask/off | accept-new | host key check for `-ssh` |
|
|
433
|
+
| `-sshnr` | — | off | with `-ssh`: do not resolve names on the jump host (use the local resolver) |
|
|
433
434
|
| `-ncf` | — | off | CSV: do not ping the CSV IP of a name without DNS record |
|
|
434
435
|
| `-up` | — | 0 | learning phase: after N rounds show only hosts seen UP (view CURRENTLY-UP; all hosts are still pinged until SET REFERENCE) |
|
|
435
436
|
| `-setref` | — | off | with `-up`: hosts seen UP become new reference list |
|
|
@@ -490,24 +491,31 @@ these rows automatically.
|
|
|
490
491
|
## SSH source (JUMPHOST)
|
|
491
492
|
|
|
492
493
|
Run fping on a jump host instead of locally (system `ssh` only). Any Linux host with a working `fping`
|
|
493
|
-
(e.g. a caretaker) will do; it resolves the names itself. Start: `-ssh [USER@]HOST[:PORT]` (+ `-sshkey FILE`, `-sshhk MODE`);
|
|
494
|
+
(e.g. a caretaker) will do; it resolves the names itself. Start: `-ssh [USER@]HOST[:PORT]` (+ `-sshkey FILE`, `-sshhk MODE`, `-sshnr`);
|
|
494
495
|
the password or key passphrase is asked on the terminal at startup. At runtime: `J` in the CLI,
|
|
495
496
|
JUMPHOST button in the web GUI (after ADV OPTIONS).
|
|
496
497
|
|
|
497
498
|

|
|
498
499
|
|
|
499
|
-
- Window: HOST, PORT, USER, PASSWORD (key passphrase), HOST KEY, KEY FILE. TEST checks ssh, login and
|
|
500
|
+
- Window: HOST, PORT, USER, PASSWORD (key passphrase), HOST KEY, NAMES, KEY FILE. TEST checks ssh, login and
|
|
500
501
|
fping on the jump host, CONNECT switches the scan source, USE LOCAL switches back. A missing local
|
|
501
502
|
`ssh` is shown in the window.
|
|
502
503
|
- HOST KEY: `accept-new` (default) - hosts seen for the first time are trusted automatically, a changed
|
|
503
504
|
key is still refused; `ask` - an unknown key is shown with its fingerprint and must be accepted with
|
|
504
505
|
TRUST KEY; `off` - no check at all (insecure).
|
|
506
|
+
- NAMES (default on): the DNS lookups eping does itself - GET NAMES, PREFER HOSTNAME / IP ADDRESS, IP ONLY -
|
|
507
|
+
run on the jump host over the same ssh connection (no second login), using `getent`, else `dig`,
|
|
508
|
+
`nslookup` or `host` there; TEST/CONNECT report which tool works. One batch per call, names are validated
|
|
509
|
+
and sent via stdin. Without a working tool, or with the option off (`-sshnr`), the local resolver is used
|
|
510
|
+
and GET NAMES is not available. The DNS caches are cleared on every source switch.
|
|
511
|
+
A name with only an AAAA record is pinged with `fping -6` there (A is preferred, as locally). Without a resolver on
|
|
512
|
+
the jump host, a name that fping reports as NO-DNS is tried with `-6` in the next round, alternating with `-4`; `-4` / `-6` force one family.
|
|
505
513
|
- The switch is applied at the start of the next round; hosts, states and counters are kept.
|
|
506
514
|
- Connection loss: results of the round are discarded, hosts keep their state, a banner is shown and
|
|
507
515
|
eping reconnects. Logged as `#COMMENT#` rows (`JUMPHOST ... OFFLINE: <reason>`, `JUMPHOST ... ONLINE
|
|
508
516
|
again`, `SOURCE SWITCH <old> -> <new>`), shown in the report's comment timeline.
|
|
509
517
|
- Log column `SOURCE`: `local` or the jump host of each row.
|
|
510
|
-
- Limits:
|
|
518
|
+
- Limits: the CSV-IP fallback is not available over ssh; at most 4 ssh processes
|
|
511
519
|
(2 on Windows); the web password transfer is plain HTTP - use `-bind 127.0.0.1` or a trusted network.
|
|
512
520
|
|
|
513
521
|
## Web GUI header
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "eping"
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.04"
|
|
8
8
|
description = "Continuous ICMP reachability monitor built on fping, with a CSV logfile analyser (epinga)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "GPL-3.0-or-later" }
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# I knew how it worked.
|
|
8
8
|
# Now, only god knows it!
|
|
9
9
|
# - - - - - - - - - - - - - - - - - - - - - - - -
|
|
10
|
-
VERSION = '4.
|
|
10
|
+
VERSION = '4.04'
|
|
11
11
|
version = VERSION # legacy alias (kept for existing references)
|
|
12
12
|
|
|
13
13
|
# --- scaling limits ---
|
|
@@ -545,13 +545,26 @@ _source_events = [] # pending comment texts (JUMPHOST OFFLINE / ONLINE, SO
|
|
|
545
545
|
_ssh_pending = None # None = no change, False = switch to local, SshCfg = switch to it
|
|
546
546
|
_ssh_lock = threading.Lock()
|
|
547
547
|
_ssh_cfgs = [] # every SshCfg that owns a temp dir (cleaned at exit)
|
|
548
|
-
_jump_last = {'host': '', 'port': '', 'user': '', 'key': '', 'hostkey': 'accept-new'} # form values of the last try (never the secret)
|
|
548
|
+
_jump_last = {'host': '', 'port': '', 'user': '', 'key': '', 'hostkey': 'accept-new', 'resolve': True} # form values of the last try (never the secret)
|
|
549
549
|
_jump_hostkeys = {} # (host, port) -> hashed known_hosts lines offered by TRUST KEY
|
|
550
550
|
|
|
551
|
+
# Name resolution on the jump host: with a working resolver tool there (getent, dig,
|
|
552
|
+
# nslookup or host - found and probed by ssh_check) the DNS lookups that eping does
|
|
553
|
+
# itself (GET NAMES, PREFER HOSTNAME / IP ADDRESS, IP ONLY) run there, over the same ssh
|
|
554
|
+
# connection, instead of on this computer. Results are cached per source.
|
|
555
|
+
JR_TOOLS = ('getent', 'dig', 'nslookup', 'host') # in order of preference
|
|
556
|
+
JR_CHUNK = 400 # names per ssh call
|
|
557
|
+
_jr_fwd = {} # name -> (ips, expires_at)
|
|
558
|
+
_jr_rev = {} # ip -> (name or None, expires_at)
|
|
559
|
+
_jr_cache_lock = threading.Lock()
|
|
560
|
+
_jr_run_lock = threading.Lock() # one remote lookup batch at a time
|
|
561
|
+
|
|
551
562
|
class SshCfg(object):
|
|
552
563
|
"""One jump host: where, how to log in, and its private temp dir (ControlPath + askpass)."""
|
|
553
|
-
def __init__(self, host, user=None, port=None, key=None, secret='', hostkey='accept-new'):
|
|
564
|
+
def __init__(self, host, user=None, port=None, key=None, secret='', hostkey='accept-new', resolve=True):
|
|
554
565
|
self.host, self.user, self.port = host, (user or None), (port or None)
|
|
566
|
+
self.resolve = bool(resolve) # wish: resolve names on the jump host
|
|
567
|
+
self.resolver = '' # tool that works there ('' = none / not checked)
|
|
555
568
|
self.hostkey = hostkey if hostkey in SSH_HOSTKEY_MODES else 'accept-new'
|
|
556
569
|
self.key = os.path.expanduser(key) if key else None
|
|
557
570
|
self.secret = secret or ''
|
|
@@ -668,24 +681,161 @@ def ssh_wrap(argv, c=None):
|
|
|
668
681
|
SSH_REMOTE_PATH, ' '.join(shlex.quote(a) for a in argv))
|
|
669
682
|
return ssh_base(c) + [c.target, 'sh -c ' + shlex.quote(inner)]
|
|
670
683
|
|
|
671
|
-
def ssh_exec(c, script, timeout=SSH_TEST_TIMEOUT):
|
|
672
|
-
"""Run a /bin/sh script on jump host c. Returns (returncode, output)."""
|
|
684
|
+
def ssh_exec(c, script, timeout=SSH_TEST_TIMEOUT, stdin_text=None):
|
|
685
|
+
"""Run a /bin/sh script on jump host c (optionally feeding stdin_text). Returns (returncode, output)."""
|
|
673
686
|
inner = 'PATH="$PATH:%s"; export PATH\n%s' % (SSH_REMOTE_PATH, script)
|
|
687
|
+
kw = {'input': stdin_text} if stdin_text is not None else {'stdin': subprocess.DEVNULL}
|
|
674
688
|
try:
|
|
675
689
|
p = subprocess.run(ssh_base(c) + [c.target, 'sh -c ' + shlex.quote(inner)],
|
|
676
|
-
|
|
677
|
-
encoding='utf-8', errors='replace', timeout=timeout, env=ssh_env(c))
|
|
690
|
+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
691
|
+
encoding='utf-8', errors='replace', timeout=timeout, env=ssh_env(c), **kw)
|
|
678
692
|
return p.returncode, p.stdout or ''
|
|
679
693
|
except FileNotFoundError:
|
|
680
694
|
return 255, 'ssh command not found'
|
|
681
695
|
except subprocess.TimeoutExpired:
|
|
682
696
|
return 255, 'ssh timeout'
|
|
683
697
|
|
|
684
|
-
_SSH_CHECK = r'''command -v
|
|
698
|
+
_SSH_CHECK = r'''RES=""; for t in getent dig nslookup host; do command -v $t >/dev/null 2>&1 && { RES=$t; break; }; done
|
|
699
|
+
echo "EPING_RESOLVER=$RES"
|
|
700
|
+
command -v fping >/dev/null 2>&1 || { echo EPING_NOFPING; exit 0; }
|
|
685
701
|
echo "EPING_VERSION=$(fping -v 2>&1 | head -1)"
|
|
686
702
|
out=$(fping -4 -c1 -t500 127.0.0.1 2>&1); echo "EPING_RC=$?"; echo "EPING_OUT=$out"
|
|
687
703
|
echo EPING_HELP_BEGIN; fping -h 2>&1; echo EPING_HELP_END'''
|
|
688
704
|
|
|
705
|
+
# One lookup per input line, up to 16 in parallel (xargs -P). The body must not contain
|
|
706
|
+
# single quotes: it is wrapped into sh -c '...'. Result line: "<input> <answers...>".
|
|
707
|
+
_JR_FWD = {
|
|
708
|
+
'getent': r'r=$(getent ahosts "$0" 2>/dev/null | awk "{print \$1}" | sort -u | tr "\n" " ")',
|
|
709
|
+
'dig': r'r=$( (dig +short +time=3 +tries=1 A "$0"; dig +short +time=3 +tries=1 AAAA "$0") 2>/dev/null | grep -E "^[0-9a-fA-F:.]+\$" | sort -u | tr "\n" " ")',
|
|
710
|
+
'nslookup': r'r=$(nslookup -timeout=3 "$0" 2>/dev/null | awk "/^Name:/{f=1} f&&/^Address/{print \$NF}" | sort -u | tr "\n" " ")',
|
|
711
|
+
'host': r'r=$(host -W 3 "$0" 2>/dev/null | awk "/has address|has IPv6 address/{print \$NF}" | sort -u | tr "\n" " ")',
|
|
712
|
+
}
|
|
713
|
+
_JR_REV = {
|
|
714
|
+
'getent': r'r=$(getent hosts "$0" 2>/dev/null | awk "NR==1{print \$2}")',
|
|
715
|
+
'dig': r'r=$(dig +short +time=3 +tries=1 -x "$0" 2>/dev/null | head -1)',
|
|
716
|
+
'nslookup': r'r=$(nslookup -timeout=3 "$0" 2>/dev/null | awk "/name = /{print \$NF; exit}")',
|
|
717
|
+
'host': r'r=$(host -W 3 "$0" 2>/dev/null | awk "/domain name pointer/{print \$NF; exit}")',
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
def _jr_run(mode, items, c):
|
|
721
|
+
"""Look up items ('fwd': names -> addresses, 'rev': addresses -> name) on jump host c
|
|
722
|
+
with its resolver tool. Returns {item: [answer tokens]}; items of a failed batch are
|
|
723
|
+
missing. Input is validated (no shell metacharacters) and goes in via stdin."""
|
|
724
|
+
body = (_JR_FWD if mode == 'fwd' else _JR_REV)[c.resolver]
|
|
725
|
+
script = ('XP=-P16; echo x | xargs -P16 -n1 true >/dev/null 2>&1 || XP=""\n'
|
|
726
|
+
"xargs -n1 $XP sh -c '" + body + '; echo "$0 $r"' + "'")
|
|
727
|
+
out = {}
|
|
728
|
+
items = [i for i in items if re.match(r'^[A-Za-z0-9._:-]{1,253}$', i)]
|
|
729
|
+
for k in range(0, len(items), JR_CHUNK):
|
|
730
|
+
chunk = items[k:k + JR_CHUNK]
|
|
731
|
+
rc, text = ssh_exec(c, script, timeout=30 + len(chunk) // 5, stdin_text='\n'.join(chunk) + '\n')
|
|
732
|
+
if rc != 0:
|
|
733
|
+
continue
|
|
734
|
+
for line in text.splitlines():
|
|
735
|
+
parts = line.split()
|
|
736
|
+
if parts and parts[0] in chunk:
|
|
737
|
+
out[parts[0]] = parts[1:]
|
|
738
|
+
return out
|
|
739
|
+
|
|
740
|
+
def jr_active():
|
|
741
|
+
"""True if lookups go to the jump host (ssh source, enabled, working resolver)."""
|
|
742
|
+
c = ssh_cfg
|
|
743
|
+
return bool(c is not None and c.resolve and c.resolver)
|
|
744
|
+
|
|
745
|
+
def _jr_ttl():
|
|
746
|
+
try:
|
|
747
|
+
return max(30.0, float(getattr(globals().get('args'), 'dns_ttl', 300)))
|
|
748
|
+
except Exception:
|
|
749
|
+
return 300.0
|
|
750
|
+
|
|
751
|
+
def jr_forward(names):
|
|
752
|
+
"""{name: [addresses]} resolved on the jump host (cached, one ssh call for all
|
|
753
|
+
uncached names). A name whose batch failed is missing in the result."""
|
|
754
|
+
c = ssh_cfg
|
|
755
|
+
if not jr_active():
|
|
756
|
+
return {}
|
|
757
|
+
now = time.time()
|
|
758
|
+
res, todo = {}, []
|
|
759
|
+
with _jr_cache_lock:
|
|
760
|
+
for n in set(names):
|
|
761
|
+
e = _jr_fwd.get(n)
|
|
762
|
+
if e and e[1] > now:
|
|
763
|
+
res[n] = e[0]
|
|
764
|
+
else:
|
|
765
|
+
todo.append(n)
|
|
766
|
+
if todo:
|
|
767
|
+
with _jr_run_lock:
|
|
768
|
+
got = _jr_run('fwd', todo, c)
|
|
769
|
+
now, ttl = time.time(), _jr_ttl()
|
|
770
|
+
with _jr_cache_lock:
|
|
771
|
+
for n in todo:
|
|
772
|
+
if n not in got:
|
|
773
|
+
res[n] = []
|
|
774
|
+
_jr_fwd[n] = ([], now + DNS_FAIL_TTL) # failed batch / bad name: retry later
|
|
775
|
+
continue
|
|
776
|
+
ips = []
|
|
777
|
+
for a in got[n]:
|
|
778
|
+
try:
|
|
779
|
+
ipaddress.ip_address(a)
|
|
780
|
+
ips.append(a)
|
|
781
|
+
except ValueError:
|
|
782
|
+
pass
|
|
783
|
+
_jr_fwd[n] = (ips, now + (ttl if ips else min(ttl, DNS_FAIL_TTL)))
|
|
784
|
+
res[n] = ips
|
|
785
|
+
return res
|
|
786
|
+
|
|
787
|
+
def jr_reverse(ips):
|
|
788
|
+
"""{ip: hostname or None} (PTR) looked up on the jump host, cached."""
|
|
789
|
+
c = ssh_cfg
|
|
790
|
+
if not jr_active():
|
|
791
|
+
return {}
|
|
792
|
+
now = time.time()
|
|
793
|
+
res, todo = {}, []
|
|
794
|
+
with _jr_cache_lock:
|
|
795
|
+
for i in set(ips):
|
|
796
|
+
e = _jr_rev.get(i)
|
|
797
|
+
if e and e[1] > now:
|
|
798
|
+
res[i] = e[0]
|
|
799
|
+
else:
|
|
800
|
+
todo.append(i)
|
|
801
|
+
if todo:
|
|
802
|
+
with _jr_run_lock:
|
|
803
|
+
got = _jr_run('rev', todo, c)
|
|
804
|
+
now, ttl = time.time(), _jr_ttl()
|
|
805
|
+
with _jr_cache_lock:
|
|
806
|
+
for i in todo:
|
|
807
|
+
if i in got:
|
|
808
|
+
nm = got[i][0].rstrip('.') if got[i] else None
|
|
809
|
+
_jr_rev[i] = (nm or None, now + (ttl if nm else min(ttl, DNS_FAIL_TTL)))
|
|
810
|
+
res[i] = nm or None
|
|
811
|
+
else:
|
|
812
|
+
res[i] = None
|
|
813
|
+
_jr_rev[i] = (None, now + DNS_FAIL_TTL)
|
|
814
|
+
return res
|
|
815
|
+
|
|
816
|
+
_ssh_fam6 = set() # hostnames tried with fping -6 on the jump host (no resolver there, see below)
|
|
817
|
+
|
|
818
|
+
def ssh_name_family(name):
|
|
819
|
+
"""Address family ('4'/'6') used for the fping run of a hostname on the jump host.
|
|
820
|
+
With a resolver there the names are looked up first (A preferred, AAAA if there is
|
|
821
|
+
no A record - same rule as locally), so an IPv6-only name is pinged with -6.
|
|
822
|
+
Without one, -4 is used and a name that fping reports as NO-DNS is tried with -6
|
|
823
|
+
in the next round (and back to -4 if that fails too), see run_ping_round()."""
|
|
824
|
+
if dns_family in ('4', '6'):
|
|
825
|
+
return dns_family
|
|
826
|
+
if jr_active():
|
|
827
|
+
ips = resolve_all_ips(name)
|
|
828
|
+
if ips:
|
|
829
|
+
return ip_version_str(ips[0]) or '4'
|
|
830
|
+
return '4'
|
|
831
|
+
return '6' if name in _ssh_fam6 else '4'
|
|
832
|
+
|
|
833
|
+
def jr_prefetch(hosts):
|
|
834
|
+
"""Resolve all hostnames of 'hosts' on the jump host in one go (fills the cache, so
|
|
835
|
+
the per-name lookups of the callers below do not each need an ssh call)."""
|
|
836
|
+
if jr_active():
|
|
837
|
+
jr_forward([h for h in hosts if not is_ip_host(h)])
|
|
838
|
+
|
|
689
839
|
def ssh_scan_hostkeys(c):
|
|
690
840
|
"""Fetch the jump host's keys (hashed known_hosts lines) and their fingerprints."""
|
|
691
841
|
lines, fps = [], []
|
|
@@ -758,6 +908,15 @@ def ssh_check(c):
|
|
|
758
908
|
m = re.search(r'EPING_HELP_BEGIN\n(.*)EPING_HELP_END', out, re.S)
|
|
759
909
|
r.update(ok=True, code='ok', help=(m.group(1) if m else ''))
|
|
760
910
|
c.help, c.version = r['help'], r['version']
|
|
911
|
+
# resolver on the jump host: tool found by the check script, then a real lookup
|
|
912
|
+
c.resolver = ''
|
|
913
|
+
tool = (re.search(r'EPING_RESOLVER=(\w*)', out) or [None, ''])[1]
|
|
914
|
+
if tool in JR_TOOLS:
|
|
915
|
+
c.resolver = tool
|
|
916
|
+
got = _jr_run('fwd', ['localhost'], c)
|
|
917
|
+
if not any(a in ('127.0.0.1', '::1') for a in got.get('localhost', [])):
|
|
918
|
+
c.resolver = ''
|
|
919
|
+
r['resolver'] = c.resolver
|
|
761
920
|
return r
|
|
762
921
|
|
|
763
922
|
def ssh_activate(c):
|
|
@@ -770,6 +929,13 @@ def ssh_activate(c):
|
|
|
770
929
|
_fping_caps = None
|
|
771
930
|
_source_down = False
|
|
772
931
|
_source_alert = ''
|
|
932
|
+
with _jr_cache_lock:
|
|
933
|
+
_jr_fwd.clear()
|
|
934
|
+
_jr_rev.clear()
|
|
935
|
+
_ssh_fam6.clear()
|
|
936
|
+
with _dns_lock: # lookups made for the old source do not apply
|
|
937
|
+
_dns_cache.clear()
|
|
938
|
+
_addr_redundancy_cache.clear()
|
|
773
939
|
if old is not None and old is not c:
|
|
774
940
|
old.close()
|
|
775
941
|
|
|
@@ -792,7 +958,7 @@ def ssh_apply_pending():
|
|
|
792
958
|
ssh_activate(p if p is not False else None)
|
|
793
959
|
_source_events.append('SOURCE SWITCH %s -> %s' % (old, SOURCE_LABEL))
|
|
794
960
|
|
|
795
|
-
def ssh_init(target, key, hostkey='accept-new'):
|
|
961
|
+
def ssh_init(target, key, hostkey='accept-new', resolve=True):
|
|
796
962
|
"""-ssh at startup: ask for the secret, trust the host key (ssh's own prompt), verify
|
|
797
963
|
that fping works on the jump host. Exits with a clear message on any problem."""
|
|
798
964
|
parsed = ssh_parse_target(target)
|
|
@@ -811,7 +977,7 @@ def ssh_init(target, key, hostkey='accept-new'):
|
|
|
811
977
|
% ((user + '@' if user else '') + host))
|
|
812
978
|
except (EOFError, KeyboardInterrupt):
|
|
813
979
|
error_handler("ERROR: -ssh: aborted")
|
|
814
|
-
c = SshCfg(host, user, port, key, secret, hostkey)
|
|
980
|
+
c = SshCfg(host, user, port, key, secret, hostkey, resolve)
|
|
815
981
|
print('SSH source %s: connecting ...' % c.label)
|
|
816
982
|
sys.stdout.flush()
|
|
817
983
|
r = ssh_check(c)
|
|
@@ -831,6 +997,9 @@ def ssh_init(target, key, hostkey='accept-new'):
|
|
|
831
997
|
error_handler("ERROR: -ssh: " + r['error'])
|
|
832
998
|
ssh_activate(c)
|
|
833
999
|
print('SSH source %s: OK (%s)' % (c.label, r['version']))
|
|
1000
|
+
print('Name resolution: ' + (('on the jump host (%s)' % c.resolver) if (c.resolve and c.resolver)
|
|
1001
|
+
else ('local (disabled by -sshnr)' if not c.resolve
|
|
1002
|
+
else 'local (no working getent/dig/nslookup/host on the jump host)')))
|
|
834
1003
|
sys.stdout.flush()
|
|
835
1004
|
|
|
836
1005
|
def jump_cfg_from(req):
|
|
@@ -851,11 +1020,13 @@ def jump_cfg_from(req):
|
|
|
851
1020
|
hk = str(req.get('hostkey', 'accept-new'))
|
|
852
1021
|
if hk not in SSH_HOSTKEY_MODES:
|
|
853
1022
|
hk = 'accept-new'
|
|
854
|
-
|
|
855
|
-
|
|
1023
|
+
resolve = str(req.get('resolve', True)).strip().lower() not in ('0', 'false', 'off', 'no')
|
|
1024
|
+
_jump_last.update(host=host, user=user, port=port, key=key, hostkey=hk, resolve=resolve)
|
|
1025
|
+
return SshCfg(host, user, int(port) if port else None, key, secret, hk, resolve), ''
|
|
856
1026
|
|
|
857
1027
|
def jump_info():
|
|
858
1028
|
return {'ssh': ssh_prereq(), 'active': SOURCE_LABEL if ssh_cfg else '', 'last': dict(_jump_last),
|
|
1029
|
+
'resolver': (ssh_cfg.resolver if (ssh_cfg and ssh_cfg.resolve) else ''),
|
|
859
1030
|
'pending': ('local' if _ssh_pending is False else (_ssh_pending.label if _ssh_pending else ''))}
|
|
860
1031
|
|
|
861
1032
|
def jump_handle(req):
|
|
@@ -873,11 +1044,18 @@ def jump_handle(req):
|
|
|
873
1044
|
e = ssh_trust_hostkeys(c.host, c.port)
|
|
874
1045
|
return dict(ok=not e, code='trust', error=e, message='' if e else 'host key trusted - press TEST')
|
|
875
1046
|
r = ssh_check(c)
|
|
1047
|
+
note = ''
|
|
1048
|
+
if r['ok']:
|
|
1049
|
+
note = ('; names are resolved on the jump host (%s)' % c.resolver if (c.resolve and c.resolver)
|
|
1050
|
+
else ('; names are resolved locally (option off)' if not c.resolve
|
|
1051
|
+
else '; names are resolved locally (no working getent/dig/nslookup/host there)'))
|
|
876
1052
|
if act == 'test' or not r['ok']:
|
|
877
1053
|
c.close()
|
|
1054
|
+
if r['ok']:
|
|
1055
|
+
r['message'] = 'OK - ' + (r['version'] or 'fping works') + note
|
|
878
1056
|
return r
|
|
879
1057
|
ssh_request(c) # connect
|
|
880
|
-
r['message'] = 'connected to %s - switching at the next round' % c.label
|
|
1058
|
+
r['message'] = 'connected to %s - switching at the next round%s' % (c.label, note)
|
|
881
1059
|
return r
|
|
882
1060
|
|
|
883
1061
|
def source_set(down, reason=''):
|
|
@@ -987,6 +1165,20 @@ def resolve_all_ips(name, family=None):
|
|
|
987
1165
|
Uncached and used only where all addresses matter (prefer-hostname redundancy
|
|
988
1166
|
check) or once per name resolution - not hot enough to need caching here."""
|
|
989
1167
|
fam = family if family is not None else dns_family
|
|
1168
|
+
if jr_active(): # the jump host's view of DNS
|
|
1169
|
+
ips = jr_forward([name]).get(name) or []
|
|
1170
|
+
v4, v6 = [], []
|
|
1171
|
+
for a in ips:
|
|
1172
|
+
ip = ipaddress.ip_address(a)
|
|
1173
|
+
if ip.version == 4:
|
|
1174
|
+
v4.append(a)
|
|
1175
|
+
elif not ip.ipv4_mapped:
|
|
1176
|
+
v6.append(a)
|
|
1177
|
+
key = lambda a: int(ipaddress.ip_address(a))
|
|
1178
|
+
for grp in ((v6, v4) if fam == '6' else (v4, v6)):
|
|
1179
|
+
if grp:
|
|
1180
|
+
return sorted(grp, key=key)
|
|
1181
|
+
return []
|
|
990
1182
|
af_list = {'4': [socket.AF_INET, socket.AF_INET6],
|
|
991
1183
|
'6': [socket.AF_INET6, socket.AF_INET]}.get(
|
|
992
1184
|
fam, [socket.AF_INET, socket.AF_INET6])
|
|
@@ -1031,6 +1223,9 @@ def get_names_for_ips(ip_list, timeout=DNS_PTR_TIMEOUT, workers=DNS_RESOLVERS):
|
|
|
1031
1223
|
this runs."""
|
|
1032
1224
|
if not ip_list:
|
|
1033
1225
|
return {}
|
|
1226
|
+
if jr_active(): # PTR lookups on the jump host
|
|
1227
|
+
got = jr_reverse(list(ip_list))
|
|
1228
|
+
return dict((ip, got.get(ip)) for ip in ip_list)
|
|
1034
1229
|
old_timeout = socket.getdefaulttimeout()
|
|
1035
1230
|
socket.setdefaulttimeout(timeout)
|
|
1036
1231
|
results = {}
|
|
@@ -1108,6 +1303,8 @@ def _forward_confirms(name, ip):
|
|
|
1108
1303
|
that resolves back to exactly this ip. Guards GET NAMES: a PTR record with no
|
|
1109
1304
|
matching forward record must not be used to rename a host, or pinging it
|
|
1110
1305
|
afterwards by name would fail (NO-DNS) or silently hit a different address."""
|
|
1306
|
+
if jr_active():
|
|
1307
|
+
return ip in (jr_forward([name]).get(name) or [])
|
|
1111
1308
|
try:
|
|
1112
1309
|
af = socket.AF_INET6 if ip_version_str(ip) == '6' else socket.AF_INET
|
|
1113
1310
|
infos = socket.getaddrinfo(name, None, af, socket.SOCK_DGRAM)
|
|
@@ -1127,6 +1324,8 @@ def _get_names_lookup(candidates):
|
|
|
1127
1324
|
ptr = get_names_for_ips(candidates)
|
|
1128
1325
|
checkable = [ip for ip in candidates if ptr.get(ip) and match_re(ptr[ip], fqdn_re)]
|
|
1129
1326
|
fwd_ok = {}
|
|
1327
|
+
if checkable and jr_active():
|
|
1328
|
+
jr_forward([ptr[ip] for ip in checkable]) # one ssh call for all forward checks
|
|
1130
1329
|
if checkable:
|
|
1131
1330
|
with concurrent.futures.ThreadPoolExecutor(
|
|
1132
1331
|
max_workers=max(1, min(DNS_RESOLVERS, len(checkable)))) as pool:
|
|
@@ -1200,6 +1399,7 @@ def apply_get_names(original_hosts_list, active_hosts_list, host_state, up_seen,
|
|
|
1200
1399
|
only used at startup (-gn), before the CLI/web loop exists to stay responsive
|
|
1201
1400
|
for. The interactive [G] key/cmd runs this in the background instead, see
|
|
1202
1401
|
get_names_start()/get_names_finish()."""
|
|
1402
|
+
jr_prefetch(original_hosts_list)
|
|
1203
1403
|
covered_ips = set()
|
|
1204
1404
|
for h in original_hosts_list:
|
|
1205
1405
|
if not is_ip_host(h):
|
|
@@ -1223,8 +1423,9 @@ def get_names_start(original_hosts_list, dns_ttl):
|
|
|
1223
1423
|
Returns (thread, result_holder, candidates); thread is None (nothing started)
|
|
1224
1424
|
when there is nothing to look up. Poll thread.is_alive() and, once False, call
|
|
1225
1425
|
get_names_finish() with result_holder."""
|
|
1226
|
-
if ssh_target:
|
|
1426
|
+
if ssh_target and not jr_active(): # reverse DNS here would not match the jump host's view
|
|
1227
1427
|
return None, None, []
|
|
1428
|
+
jr_prefetch(original_hosts_list)
|
|
1228
1429
|
covered_ips = set()
|
|
1229
1430
|
for h in original_hosts_list:
|
|
1230
1431
|
if not is_ip_host(h):
|
|
@@ -1847,6 +2048,7 @@ def apply_prefer_hostname(hosts_list, dns_ttl):
|
|
|
1847
2048
|
hostname's resolved addresses are cached for this check - see _cached_all_ips()."""
|
|
1848
2049
|
hostname_ips = set()
|
|
1849
2050
|
names = [h for h in hosts_list if not is_ip_host(h)]
|
|
2051
|
+
jr_prefetch(names)
|
|
1850
2052
|
if len(names) > 1: # resolve in parallel - serial lookups of unresolvable names are slow
|
|
1851
2053
|
with concurrent.futures.ThreadPoolExecutor(max_workers=32) as ex:
|
|
1852
2054
|
for ips in ex.map(lambda n: _cached_all_ips(n, dns_ttl), names):
|
|
@@ -1866,6 +2068,7 @@ def apply_prefer_ip(hosts_list, dns_ttl):
|
|
|
1866
2068
|
raw_ips = set(h for h in hosts_list if is_ip_host(h))
|
|
1867
2069
|
if not raw_ips:
|
|
1868
2070
|
return list(hosts_list)
|
|
2071
|
+
jr_prefetch(hosts_list)
|
|
1869
2072
|
return [h for h in hosts_list
|
|
1870
2073
|
if is_ip_host(h) or not raw_ips.intersection(_cached_all_ips(h, dns_ttl))]
|
|
1871
2074
|
|
|
@@ -1885,6 +2088,7 @@ def apply_ip_only_on(original_hosts_list, active_hosts_list, host_state, up_seen
|
|
|
1885
2088
|
entries."""
|
|
1886
2089
|
existing = set(h.lower() for h in original_hosts_list)
|
|
1887
2090
|
ip_map, unresolved, dropped = {}, 0, 0
|
|
2091
|
+
jr_prefetch(original_hosts_list)
|
|
1888
2092
|
for h in list(original_hosts_list):
|
|
1889
2093
|
if is_ip_host(h):
|
|
1890
2094
|
continue
|
|
@@ -1939,6 +2143,7 @@ def apply_ip_only_on_web(original_hosts_list, active_hosts_list, host_state,
|
|
|
1939
2143
|
uses apply_ip_only_on above and keeps the old drop-on-collision behaviour."""
|
|
1940
2144
|
existing = set(h.lower() for h in original_hosts_list)
|
|
1941
2145
|
ip_map, unresolved, skipped = {}, 0, 0
|
|
2146
|
+
jr_prefetch(original_hosts_list)
|
|
1942
2147
|
for h in list(original_hosts_list):
|
|
1943
2148
|
if is_ip_host(h):
|
|
1944
2149
|
continue
|
|
@@ -2739,12 +2944,14 @@ def run_ping_round(active_hosts_list, threads_arg, rate_pps=DEFAULT_RATE_PPS,
|
|
|
2739
2944
|
# group above is split again by address family. In the common single-family
|
|
2740
2945
|
# case this yields exactly one subgroup per group, same as before IPv6 support.
|
|
2741
2946
|
subgroups = [] # (targets, retries, label, family)
|
|
2947
|
+
if ssh_target and jr_active():
|
|
2948
|
+
jr_prefetch(targets) # one ssh call for all uncached names
|
|
2742
2949
|
for grp_targets, grp_retries, grp_name in groups:
|
|
2743
2950
|
by_fam = {'4': [], '6': []}
|
|
2744
2951
|
for t in grp_targets:
|
|
2745
2952
|
fam = ip_version_str(t)
|
|
2746
2953
|
if not fam and ssh_target: # name resolved on the jump host
|
|
2747
|
-
fam =
|
|
2954
|
+
fam = ssh_name_family(t)
|
|
2748
2955
|
if fam:
|
|
2749
2956
|
by_fam[fam].append(t)
|
|
2750
2957
|
for fam in ('4', '6'):
|
|
@@ -2842,6 +3049,13 @@ def run_ping_round(active_hosts_list, threads_arg, rate_pps=DEFAULT_RATE_PPS,
|
|
|
2842
3049
|
new_row[0] = display_name
|
|
2843
3050
|
rows.append(new_row)
|
|
2844
3051
|
|
|
3052
|
+
if ssh_target and not jr_active() and dns_family == 'auto':
|
|
3053
|
+
for r in rows: # alternate -4 / -6 for names fping cannot resolve
|
|
3054
|
+
if not is_ip_host(r[0]) and 'NO-DNS' in r[1]:
|
|
3055
|
+
if r[0] in _ssh_fam6:
|
|
3056
|
+
_ssh_fam6.discard(r[0])
|
|
3057
|
+
else:
|
|
3058
|
+
_ssh_fam6.add(r[0])
|
|
2845
3059
|
# names that do not resolve never reach fping - report them like fping would
|
|
2846
3060
|
if unresolved:
|
|
2847
3061
|
now_str = get_date_time()
|
|
@@ -3306,6 +3520,7 @@ WEB_INDEX_HTML = r"""<!DOCTYPE html>
|
|
|
3306
3520
|
<option value="accept-new">accept new hosts automatically (a changed key is still refused)</option>
|
|
3307
3521
|
<option value="ask">ask - confirm an unknown host key (TRUST KEY)</option>
|
|
3308
3522
|
<option value="off">off - no host key check at all (insecure)</option></select></td></tr>
|
|
3523
|
+
<tr><td style="padding:3px 8px 3px 0;white-space:nowrap">NAMES</td><td colspan="3"><label style="font-size:12px;cursor:pointer"><input type="checkbox" id="jumpResolve" checked> resolve names on the jump host (getent / dig / nslookup / host; for GET NAMES, PREFER HOSTNAME / IP ADDRESS, IP ONLY)</label></td></tr>
|
|
3309
3524
|
<tr><td style="padding:3px 8px 3px 0;white-space:nowrap">KEY FILE</td><td colspan="3"><input type="text" id="jumpKey" style="width:100%;box-sizing:border-box" placeholder="optional, path on the computer running eping.py" autocomplete="off" spellcheck="false"></td></tr>
|
|
3310
3525
|
</table>
|
|
3311
3526
|
<div id="jumpWarn" style="display:none;font-size:11px;color:var(--flap,#d9a400);margin-top:6px">This page is not served from this computer (HTTP, no encryption): the password would cross the network in clear text. Prefer a key.</div>
|
|
@@ -4610,6 +4825,7 @@ function jumpFields(action){
|
|
|
4610
4825
|
return {action: action, host: document.getElementById('jumpHost').value, port: document.getElementById('jumpPort').value,
|
|
4611
4826
|
user: document.getElementById('jumpUser').value, key: document.getElementById('jumpKey').value,
|
|
4612
4827
|
hostkey: document.getElementById('jumpHostKey').value,
|
|
4828
|
+
resolve: document.getElementById('jumpResolve').checked,
|
|
4613
4829
|
secret: document.getElementById('jumpSecret').value};
|
|
4614
4830
|
}
|
|
4615
4831
|
function jumpButtons(){
|
|
@@ -4647,13 +4863,15 @@ function openJump(){
|
|
|
4647
4863
|
if(j.ssh && j.ssh.ok){
|
|
4648
4864
|
pre.style.color = 'var(--dim)';
|
|
4649
4865
|
pre.textContent = 'ssh client: ' + (j.ssh.version || 'found') + (j.ssh.keyscan ? '' : ' (ssh-keyscan missing: unknown host keys cannot be trusted here)') +
|
|
4650
|
-
' | source: ' + (j.active ? j.active : 'local') + (j.pending ? ' (switching to ' + j.pending + ')' : '')
|
|
4866
|
+
' | source: ' + (j.active ? j.active : 'local') + (j.pending ? ' (switching to ' + j.pending + ')' : '') +
|
|
4867
|
+
(j.active ? ' | names: ' + (j.resolver ? 'jump host (' + j.resolver + ')' : 'local') : '');
|
|
4651
4868
|
} else {
|
|
4652
4869
|
pre.style.color = 'var(--down,#f85149)';
|
|
4653
4870
|
pre.textContent = (j.ssh && j.ssh.hint) || 'OpenSSH client not found on this computer.';
|
|
4654
4871
|
}
|
|
4655
4872
|
if(j.last){
|
|
4656
4873
|
if(j.last.hostkey) document.getElementById('jumpHostKey').value = j.last.hostkey;
|
|
4874
|
+
if(typeof j.last.resolve === 'boolean') document.getElementById('jumpResolve').checked = j.last.resolve;
|
|
4657
4875
|
['host','port','user','key'].forEach(function(k){
|
|
4658
4876
|
var el = document.getElementById('jump' + k.charAt(0).toUpperCase() + k.slice(1));
|
|
4659
4877
|
if(el && !el.value) el.value = j.last[k] || '';
|
|
@@ -7189,6 +7407,8 @@ def build_cli_snapshot(args, backoff, timeout, retries, down_retries, flap_windo
|
|
|
7189
7407
|
parts.append('-sshkey ' + args.ssh_key)
|
|
7190
7408
|
if getattr(args, 'ssh_hostkey', 'accept-new') != 'accept-new':
|
|
7191
7409
|
parts.append('-sshhk ' + args.ssh_hostkey)
|
|
7410
|
+
if getattr(args, 'ssh_no_resolve', False):
|
|
7411
|
+
parts.append('-sshnr')
|
|
7192
7412
|
return ' '.join(parts)
|
|
7193
7413
|
|
|
7194
7414
|
|
|
@@ -7379,7 +7599,8 @@ def run_web_mode(original_hosts_list, host_state, args, logfile_file_name,
|
|
|
7379
7599
|
gn_thread, gn_result, gn_candidates = get_names_start(
|
|
7380
7600
|
original_hosts_list, int(args.dns_ttl))
|
|
7381
7601
|
if gn_thread is None:
|
|
7382
|
-
message = 'get names: no
|
|
7602
|
+
message = ('get names: not available - the jump host has no working resolver (or it is switched off)'
|
|
7603
|
+
if (ssh_target and not jr_active()) else 'get names: no eligible IP host(s)')
|
|
7383
7604
|
else:
|
|
7384
7605
|
message = ('get names: running in background (%d host(s))'
|
|
7385
7606
|
% len(gn_candidates))
|
|
@@ -7961,6 +8182,7 @@ if __name__=='__main__':
|
|
|
7961
8182
|
parser.add_argument('-fs', '--full_sweep', default=str(FULL_SWEEP_DEF), dest='full_sweep', help="every Nth run probes every host with full retries, range 0-" + str(FULL_SWEEP_MAX) + " (default: " + str(FULL_SWEEP_DEF) + ", 0 = disabled/never)")
|
|
7962
8183
|
parser.add_argument('-ssh', '--ssh', dest='ssh', metavar='[USER@]HOST[:PORT]', default=None, help="run fping on this jump host via ssh instead of locally (system ssh; password or key passphrase is asked at startup; the jump host resolves the names and needs a working fping)")
|
|
7963
8184
|
parser.add_argument('-sshkey', '--ssh_key', dest='ssh_key', metavar='FILE', default=None, help="private key file for -ssh (default: ssh agent / default keys)")
|
|
8185
|
+
parser.add_argument('-sshnr', '--ssh_no_resolve', action="store_true", dest='ssh_no_resolve', help="with -ssh: do not resolve names on the jump host (default: GET NAMES, PREFER HOSTNAME / IP ADDRESS and IP ONLY use getent / dig / nslookup / host there when available; otherwise the local resolver is used)")
|
|
7964
8186
|
parser.add_argument('-sshhk', '--ssh_hostkey', dest='ssh_hostkey', choices=SSH_HOSTKEY_MODES, default='accept-new', help="host key check for -ssh: accept-new = trust hosts seen for the first time, a changed key is refused (default); ask = confirm an unknown key; off = no check at all (insecure)")
|
|
7965
8187
|
parser.add_argument('-ncf', '--no_csv_fallback', action="store_true", dest='no_csv_fallback', help="do not ping the CSV address of a host whose name does not resolve (default: it is pinged by the CSV IP and marked in the host popup)")
|
|
7966
8188
|
parser.add_argument('-ncs', '--no_check_source', action="store_true", dest='no_check_source', help="do not pass --check-source to fping (only needed for hosts replying from a different address)")
|
|
@@ -8327,7 +8549,7 @@ if __name__=='__main__':
|
|
|
8327
8549
|
# (CLI: [A]=ADD HOST, web gui: ADD HOST / UPLOAD FILE)
|
|
8328
8550
|
|
|
8329
8551
|
if args.ssh:
|
|
8330
|
-
ssh_init(args.ssh, args.ssh_key, args.ssh_hostkey)
|
|
8552
|
+
ssh_init(args.ssh, args.ssh_key, args.ssh_hostkey, not args.ssh_no_resolve)
|
|
8331
8553
|
elif args.ssh_key:
|
|
8332
8554
|
error_handler("ERROR: -sshkey needs -ssh")
|
|
8333
8555
|
|
|
@@ -8575,13 +8797,15 @@ if __name__=='__main__':
|
|
|
8575
8797
|
Same actions as the web gui window (jump_handle). Pinging keeps running meanwhile."""
|
|
8576
8798
|
rows, cols = screen.getmaxyx()
|
|
8577
8799
|
dw = min(76, max(40, cols - 4))
|
|
8578
|
-
dh =
|
|
8800
|
+
dh = 19
|
|
8579
8801
|
dy = max(0, rows // 2 - dh // 2)
|
|
8580
8802
|
dx = max(0, cols // 2 - dw // 2)
|
|
8581
|
-
labels = ['HOST', 'PORT', 'USER', 'PASSWORD', 'HOST KEY', 'KEY FILE']
|
|
8582
|
-
keys = ['host', 'port', 'user', 'secret', 'hostkey', 'key']
|
|
8803
|
+
labels = ['HOST', 'PORT', 'USER', 'PASSWORD', 'HOST KEY', 'NAMES', 'KEY FILE']
|
|
8804
|
+
keys = ['host', 'port', 'user', 'secret', 'hostkey', 'resolve', 'key']
|
|
8583
8805
|
vals = [_jump_last.get('host', ''), _jump_last.get('port', ''), _jump_last.get('user', ''),
|
|
8584
|
-
'', _jump_last.get('hostkey', 'accept-new'),
|
|
8806
|
+
'', _jump_last.get('hostkey', 'accept-new'),
|
|
8807
|
+
'on' if _jump_last.get('resolve', True) else 'off', _jump_last.get('key', '')]
|
|
8808
|
+
nm_text = {'on': 'resolve on the jump host', 'off': 'resolve locally'}
|
|
8585
8809
|
hk_text = {'ask': 'ask (confirm unknown key)', 'accept-new': 'accept new hosts', 'off': 'OFF - no check (insecure)'}
|
|
8586
8810
|
pre = ssh_prereq()
|
|
8587
8811
|
status = ['']
|
|
@@ -8627,11 +8851,13 @@ if __name__=='__main__':
|
|
|
8627
8851
|
screen_output(dy + 3, dx, '│' + (' ' + info)[:dw - 2].ljust(dw - 2) + '│', cls, 0)
|
|
8628
8852
|
fw = dw - 16
|
|
8629
8853
|
for i, lab in enumerate(labels):
|
|
8630
|
-
shown = ('*' * len(vals[i])) if keys[i] == 'secret' else (
|
|
8854
|
+
shown = ('*' * len(vals[i])) if keys[i] == 'secret' else (
|
|
8855
|
+
hk_text[vals[i]] + ' [ENTER/SPACE=change]' if keys[i] == 'hostkey' else
|
|
8856
|
+
nm_text[vals[i]] + ' [ENTER/SPACE=change]' if keys[i] == 'resolve' else vals[i])
|
|
8631
8857
|
shown = shown[-fw:] if len(shown) > fw else shown
|
|
8632
8858
|
line = ' ' + lab.ljust(10) + (shown + '_' * 0).ljust(fw) + ' '
|
|
8633
8859
|
screen_output(dy + 4 + i, dx, '│' + line[:dw - 2].ljust(dw - 2) + '│', 2 if focus == i else 1, 1 if focus == i else 0)
|
|
8634
|
-
screen_output(dy +
|
|
8860
|
+
screen_output(dy + 11, dx, '│' + ' ' * (dw - 2) + '│', 1, 0)
|
|
8635
8861
|
lines = []
|
|
8636
8862
|
for t in status:
|
|
8637
8863
|
lines.extend(wrap(t, dw - 4))
|
|
@@ -8639,18 +8865,18 @@ if __name__=='__main__':
|
|
|
8639
8865
|
lines.extend(wrap(f, dw - 4))
|
|
8640
8866
|
for i in range(4):
|
|
8641
8867
|
txt = lines[i] if i < len(lines) else ''
|
|
8642
|
-
screen_output(dy +
|
|
8868
|
+
screen_output(dy + 12 + i, dx, '│' + (' ' + txt)[:dw - 2].ljust(dw - 2) + '│', status_cls, 0)
|
|
8643
8869
|
bl = buttons()
|
|
8644
8870
|
col = dx + 2
|
|
8645
|
-
screen_output(dy +
|
|
8871
|
+
screen_output(dy + 16, dx, '│' + ' ' * (dw - 2) + '│', 1, 0)
|
|
8646
8872
|
for i, (name, _a) in enumerate(bl):
|
|
8647
8873
|
lab = ' [' + name + '] '
|
|
8648
8874
|
hi = (focus == len(labels) + i)
|
|
8649
|
-
screen_output(dy +
|
|
8875
|
+
screen_output(dy + 16, col, lab, 2 if hi else 1, 1 if hi else 0)
|
|
8650
8876
|
col += len(lab) + 1
|
|
8651
|
-
screen_output(dy +
|
|
8652
|
-
screen_output(dy +
|
|
8653
|
-
if focus < len(labels) and keys[focus]
|
|
8877
|
+
screen_output(dy + 17, dx, '│' + ' TAB/arrows=move ENTER=select ESC=close'.ljust(dw - 2) + '│', 1, 0)
|
|
8878
|
+
screen_output(dy + 18, dx, '└' + '─' * (dw - 2) + '┘', 1, 1)
|
|
8879
|
+
if focus < len(labels) and keys[focus] not in ('hostkey', 'resolve') and not busy['t']:
|
|
8654
8880
|
curses.curs_set(1)
|
|
8655
8881
|
shown_len = min(len(vals[focus]), fw)
|
|
8656
8882
|
screen.move(dy + 4 + focus, dx + 12 + shown_len)
|
|
@@ -8722,6 +8948,8 @@ if __name__=='__main__':
|
|
|
8722
8948
|
focus = (focus - 1) % nfocus
|
|
8723
8949
|
elif ch in (10, 13, 32) and focus < len(labels) and keys[focus] == 'hostkey':
|
|
8724
8950
|
vals[focus] = SSH_HOSTKEY_MODES[(SSH_HOSTKEY_MODES.index(vals[focus]) + 1) % 3]
|
|
8951
|
+
elif ch in (10, 13, 32) and focus < len(labels) and keys[focus] == 'resolve':
|
|
8952
|
+
vals[focus] = 'off' if vals[focus] == 'on' else 'on'
|
|
8725
8953
|
elif ch in (10, 13):
|
|
8726
8954
|
if focus < len(labels):
|
|
8727
8955
|
focus = (focus + 1) % nfocus
|
|
@@ -8733,7 +8961,7 @@ if __name__=='__main__':
|
|
|
8733
8961
|
status, status_cls = [pre['hint']], 3
|
|
8734
8962
|
else:
|
|
8735
8963
|
start(act)
|
|
8736
|
-
elif focus < len(labels):
|
|
8964
|
+
elif focus < len(labels) and keys[focus] not in ('hostkey', 'resolve'):
|
|
8737
8965
|
if ch in (curses.KEY_BACKSPACE, 127, 8):
|
|
8738
8966
|
vals[focus] = vals[focus][:-1]
|
|
8739
8967
|
elif 32 <= ch <= 126 and len(vals[focus]) < 255:
|
|
@@ -9529,7 +9757,8 @@ if __name__=='__main__':
|
|
|
9529
9757
|
gn_thread, gn_result, gn_candidates = get_names_start(
|
|
9530
9758
|
original_hosts_list, int(args.dns_ttl))
|
|
9531
9759
|
if gn_thread is None:
|
|
9532
|
-
notice('GET NAMES:
|
|
9760
|
+
notice('GET NAMES: NOT AVAILABLE VIA JUMP HOST' if (ssh_target and not jr_active())
|
|
9761
|
+
else 'GET NAMES: NO ELIGIBLE IP HOST(S)', 2, 3)
|
|
9533
9762
|
message = 'get names: no eligible ip host(s)'
|
|
9534
9763
|
else:
|
|
9535
9764
|
notice('GET NAMES: RUNNING IN BACKGROUND (%d HOST(S))'
|
|
@@ -10307,7 +10536,8 @@ if __name__=='__main__':
|
|
|
10307
10536
|
gn_thread, gn_result, gn_candidates = get_names_start(
|
|
10308
10537
|
original_hosts_list, int(args.dns_ttl))
|
|
10309
10538
|
if gn_thread is None:
|
|
10310
|
-
notice('GET NAMES:
|
|
10539
|
+
notice('GET NAMES: NOT AVAILABLE VIA JUMP HOST' if (ssh_target and not jr_active())
|
|
10540
|
+
else 'GET NAMES: NO ELIGIBLE IP HOST(S)', 2, 3)
|
|
10311
10541
|
else:
|
|
10312
10542
|
notice('GET NAMES: RUNNING IN BACKGROUND (%d HOST(S))'
|
|
10313
10543
|
% len(gn_candidates), 2)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eping
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.4
|
|
4
4
|
Summary: Continuous ICMP reachability monitor built on fping, with a CSV logfile analyser (epinga)
|
|
5
5
|
Author-email: Ewald Jeitler <ewald@jeitler.cc>
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Dynamic: license-file
|
|
19
19
|
|
|
20
|
-
# eping.py 4.
|
|
20
|
+
# eping.py 4.04
|
|
21
21
|
|
|
22
22
|
Continuous ICMP reachability monitor built on `fping`. Scans a host list in a loop,
|
|
23
23
|
reports each host UP/DOWN/NO-DNS, counts state changes. CLI (curses) or web GUI.
|
|
@@ -449,6 +449,7 @@ separated) · `-r` IP range (comma separated, shortened end)
|
|
|
449
449
|
| `-ssh` | `[USER@]HOST[:PORT]` | off | run fping on a jump host via ssh (see *SSH source (JUMPHOST)*) |
|
|
450
450
|
| `-sshkey` | file | agent | private key file for `-ssh` |
|
|
451
451
|
| `-sshhk` | accept-new/ask/off | accept-new | host key check for `-ssh` |
|
|
452
|
+
| `-sshnr` | — | off | with `-ssh`: do not resolve names on the jump host (use the local resolver) |
|
|
452
453
|
| `-ncf` | — | off | CSV: do not ping the CSV IP of a name without DNS record |
|
|
453
454
|
| `-up` | — | 0 | learning phase: after N rounds show only hosts seen UP (view CURRENTLY-UP; all hosts are still pinged until SET REFERENCE) |
|
|
454
455
|
| `-setref` | — | off | with `-up`: hosts seen UP become new reference list |
|
|
@@ -509,24 +510,31 @@ these rows automatically.
|
|
|
509
510
|
## SSH source (JUMPHOST)
|
|
510
511
|
|
|
511
512
|
Run fping on a jump host instead of locally (system `ssh` only). Any Linux host with a working `fping`
|
|
512
|
-
(e.g. a caretaker) will do; it resolves the names itself. Start: `-ssh [USER@]HOST[:PORT]` (+ `-sshkey FILE`, `-sshhk MODE`);
|
|
513
|
+
(e.g. a caretaker) will do; it resolves the names itself. Start: `-ssh [USER@]HOST[:PORT]` (+ `-sshkey FILE`, `-sshhk MODE`, `-sshnr`);
|
|
513
514
|
the password or key passphrase is asked on the terminal at startup. At runtime: `J` in the CLI,
|
|
514
515
|
JUMPHOST button in the web GUI (after ADV OPTIONS).
|
|
515
516
|
|
|
516
517
|

|
|
517
518
|
|
|
518
|
-
- Window: HOST, PORT, USER, PASSWORD (key passphrase), HOST KEY, KEY FILE. TEST checks ssh, login and
|
|
519
|
+
- Window: HOST, PORT, USER, PASSWORD (key passphrase), HOST KEY, NAMES, KEY FILE. TEST checks ssh, login and
|
|
519
520
|
fping on the jump host, CONNECT switches the scan source, USE LOCAL switches back. A missing local
|
|
520
521
|
`ssh` is shown in the window.
|
|
521
522
|
- HOST KEY: `accept-new` (default) - hosts seen for the first time are trusted automatically, a changed
|
|
522
523
|
key is still refused; `ask` - an unknown key is shown with its fingerprint and must be accepted with
|
|
523
524
|
TRUST KEY; `off` - no check at all (insecure).
|
|
525
|
+
- NAMES (default on): the DNS lookups eping does itself - GET NAMES, PREFER HOSTNAME / IP ADDRESS, IP ONLY -
|
|
526
|
+
run on the jump host over the same ssh connection (no second login), using `getent`, else `dig`,
|
|
527
|
+
`nslookup` or `host` there; TEST/CONNECT report which tool works. One batch per call, names are validated
|
|
528
|
+
and sent via stdin. Without a working tool, or with the option off (`-sshnr`), the local resolver is used
|
|
529
|
+
and GET NAMES is not available. The DNS caches are cleared on every source switch.
|
|
530
|
+
A name with only an AAAA record is pinged with `fping -6` there (A is preferred, as locally). Without a resolver on
|
|
531
|
+
the jump host, a name that fping reports as NO-DNS is tried with `-6` in the next round, alternating with `-4`; `-4` / `-6` force one family.
|
|
524
532
|
- The switch is applied at the start of the next round; hosts, states and counters are kept.
|
|
525
533
|
- Connection loss: results of the round are discarded, hosts keep their state, a banner is shown and
|
|
526
534
|
eping reconnects. Logged as `#COMMENT#` rows (`JUMPHOST ... OFFLINE: <reason>`, `JUMPHOST ... ONLINE
|
|
527
535
|
again`, `SOURCE SWITCH <old> -> <new>`), shown in the report's comment timeline.
|
|
528
536
|
- Log column `SOURCE`: `local` or the jump host of each row.
|
|
529
|
-
- Limits:
|
|
537
|
+
- Limits: the CSV-IP fallback is not available over ssh; at most 4 ssh processes
|
|
530
538
|
(2 on Windows); the web password transfer is plain HTTP - use `-bind 127.0.0.1` or a trusted network.
|
|
531
539
|
|
|
532
540
|
## Web GUI header
|
{eping-4.2 → eping-4.4}/LICENSE
RENAMED
|
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
|