eping 4.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eping
3
- Version: 4.1
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.01
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.
@@ -235,8 +235,8 @@ Single self-contained page, no external resources.
235
235
 
236
236
  - Table: CLI-style columns, filled top-down then next column right. Horizontal scroll
237
237
  if needed ("SCROLL RIGHT FOR MORE" hint in the stats bar).
238
- - Footer stats: HOSTS, RUNTIME, HOST UP/DOWN/FLAPPING, for the whole current view regardless
239
- of the match filter and the ADV FILTER (HOSTS-SHOWN = shown count). The CLI footer counts the same way. RUNTIME shows `n/a` briefly during a background round.
238
+ - Footer stats: HOSTS, RUNTIME, HOST UP/DOWN/FLAPPING always count all hosts; SHOWN is the
239
+ number of rows displayed after the view, the match filter and the ADV FILTER. The CLI footer counts the same way. RUNTIME shows `n/a` briefly during a background round.
240
240
  - Adding many hosts (`ADD`, ADD HOSTS FILE, upload) shows "added N host(s) - please wait,
241
241
  pinging ..." until they appear with a state, then `done`.
242
242
  - Font size 6–28px (slider, `A−`/`A+`, `+`/`−` keys), stored in `localStorage`.
@@ -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
  ![JUMPHOST window](https://raw.githubusercontent.com/ewaldj/eping/main/docs/img/jumphost.png)
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: GET NAMES and the CSV-IP fallback are not available over ssh; at most 4 ssh processes
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.01
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.
@@ -216,8 +216,8 @@ Single self-contained page, no external resources.
216
216
 
217
217
  - Table: CLI-style columns, filled top-down then next column right. Horizontal scroll
218
218
  if needed ("SCROLL RIGHT FOR MORE" hint in the stats bar).
219
- - Footer stats: HOSTS, RUNTIME, HOST UP/DOWN/FLAPPING, for the whole current view regardless
220
- of the match filter and the ADV FILTER (HOSTS-SHOWN = shown count). The CLI footer counts the same way. RUNTIME shows `n/a` briefly during a background round.
219
+ - Footer stats: HOSTS, RUNTIME, HOST UP/DOWN/FLAPPING always count all hosts; SHOWN is the
220
+ number of rows displayed after the view, the match filter and the ADV FILTER. The CLI footer counts the same way. RUNTIME shows `n/a` briefly during a background round.
221
221
  - Adding many hosts (`ADD`, ADD HOSTS FILE, upload) shows "added N host(s) - please wait,
222
222
  pinging ..." until they appear with a state, then `done`.
223
223
  - Font size 6–28px (slider, `A−`/`A+`, `+`/`−` keys), stored in `localStorage`.
@@ -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
  ![JUMPHOST window](https://raw.githubusercontent.com/ewaldj/eping/main/docs/img/jumphost.png)
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: GET NAMES and the CSV-IP fallback are not available over ssh; at most 4 ssh processes
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.01"
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.01'
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
- stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
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 fping >/dev/null 2>&1 || { echo EPING_NOFPING; exit 0; }
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
- _jump_last.update(host=host, user=user, port=port, key=key, hostkey=hk)
855
- return SshCfg(host, user, int(port) if port else None, key, secret, hk), ''
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: # reverse DNS here would not match the jump host's view
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):
@@ -1716,6 +1917,13 @@ def view_hosts(mode, hosts, host_state, tz_offset, flap_window=FLAP_WINDOW_DEF):
1716
1917
  return hosts
1717
1918
  return filter_hosts(mode, hosts, host_state, tz_offset, flap_window)
1718
1919
 
1920
+ def view_rows(rows, mode, host_state, tz_offset, flap_window=FLAP_WINDOW_DEF):
1921
+ """Rows (from build_display) of the hosts shown by the view 'mode' (live)."""
1922
+ if mode <= 0:
1923
+ return rows
1924
+ keep = set(filter_hosts(mode, [r[0] for r in rows], host_state, tz_offset, flap_window))
1925
+ return [r for r in rows if r[0] in keep]
1926
+
1719
1927
  def filter_hosts(mode, original_hosts_list, host_state, tz_offset,
1720
1928
  flap_window=FLAP_WINDOW_DEF, up_seen=None):
1721
1929
  """Host list for the given view mode - a snapshot, taken when the view switches.
@@ -1840,6 +2048,7 @@ def apply_prefer_hostname(hosts_list, dns_ttl):
1840
2048
  hostname's resolved addresses are cached for this check - see _cached_all_ips()."""
1841
2049
  hostname_ips = set()
1842
2050
  names = [h for h in hosts_list if not is_ip_host(h)]
2051
+ jr_prefetch(names)
1843
2052
  if len(names) > 1: # resolve in parallel - serial lookups of unresolvable names are slow
1844
2053
  with concurrent.futures.ThreadPoolExecutor(max_workers=32) as ex:
1845
2054
  for ips in ex.map(lambda n: _cached_all_ips(n, dns_ttl), names):
@@ -1859,6 +2068,7 @@ def apply_prefer_ip(hosts_list, dns_ttl):
1859
2068
  raw_ips = set(h for h in hosts_list if is_ip_host(h))
1860
2069
  if not raw_ips:
1861
2070
  return list(hosts_list)
2071
+ jr_prefetch(hosts_list)
1862
2072
  return [h for h in hosts_list
1863
2073
  if is_ip_host(h) or not raw_ips.intersection(_cached_all_ips(h, dns_ttl))]
1864
2074
 
@@ -1878,6 +2088,7 @@ def apply_ip_only_on(original_hosts_list, active_hosts_list, host_state, up_seen
1878
2088
  entries."""
1879
2089
  existing = set(h.lower() for h in original_hosts_list)
1880
2090
  ip_map, unresolved, dropped = {}, 0, 0
2091
+ jr_prefetch(original_hosts_list)
1881
2092
  for h in list(original_hosts_list):
1882
2093
  if is_ip_host(h):
1883
2094
  continue
@@ -1932,6 +2143,7 @@ def apply_ip_only_on_web(original_hosts_list, active_hosts_list, host_state,
1932
2143
  uses apply_ip_only_on above and keeps the old drop-on-collision behaviour."""
1933
2144
  existing = set(h.lower() for h in original_hosts_list)
1934
2145
  ip_map, unresolved, skipped = {}, 0, 0
2146
+ jr_prefetch(original_hosts_list)
1935
2147
  for h in list(original_hosts_list):
1936
2148
  if is_ip_host(h):
1937
2149
  continue
@@ -2732,12 +2944,14 @@ def run_ping_round(active_hosts_list, threads_arg, rate_pps=DEFAULT_RATE_PPS,
2732
2944
  # group above is split again by address family. In the common single-family
2733
2945
  # case this yields exactly one subgroup per group, same as before IPv6 support.
2734
2946
  subgroups = [] # (targets, retries, label, family)
2947
+ if ssh_target and jr_active():
2948
+ jr_prefetch(targets) # one ssh call for all uncached names
2735
2949
  for grp_targets, grp_retries, grp_name in groups:
2736
2950
  by_fam = {'4': [], '6': []}
2737
2951
  for t in grp_targets:
2738
2952
  fam = ip_version_str(t)
2739
2953
  if not fam and ssh_target: # name resolved on the jump host
2740
- fam = '6' if dns_family == '6' else '4'
2954
+ fam = ssh_name_family(t)
2741
2955
  if fam:
2742
2956
  by_fam[fam].append(t)
2743
2957
  for fam in ('4', '6'):
@@ -2835,6 +3049,13 @@ def run_ping_round(active_hosts_list, threads_arg, rate_pps=DEFAULT_RATE_PPS,
2835
3049
  new_row[0] = display_name
2836
3050
  rows.append(new_row)
2837
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])
2838
3059
  # names that do not resolve never reach fping - report them like fping would
2839
3060
  if unresolved:
2840
3061
  now_str = get_date_time()
@@ -3299,6 +3520,7 @@ WEB_INDEX_HTML = r"""<!DOCTYPE html>
3299
3520
  <option value="accept-new">accept new hosts automatically (a changed key is still refused)</option>
3300
3521
  <option value="ask">ask - confirm an unknown host key (TRUST KEY)</option>
3301
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>
3302
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>
3303
3525
  </table>
3304
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>
@@ -3433,7 +3655,7 @@ WEB_INDEX_HTML = r"""<!DOCTYPE html>
3433
3655
  <span id="ctrls">
3434
3656
  <span class="ctrls-row" id="ctrlsRow1">
3435
3657
  <span id="ctrlsMain">
3436
- <select id="selFilter" title="choose which hosts are shown and pinged - a host outside the current view is not checked until a view including it is selected again">
3658
+ <select id="selFilter" title="display filter: choose which hosts are shown (evaluated live) - all hosts keep being pinged until SET REFERENCE removes the hosts that are not shown">
3437
3659
  <option value="0">ALL HOSTS</option>
3438
3660
  <option value="1">CURRENTLY-UP</option>
3439
3661
  <option value="3">ALWAYS-UP</option>
@@ -3451,7 +3673,7 @@ WEB_INDEX_HTML = r"""<!DOCTYPE html>
3451
3673
  <option value="3">FLAP/UP/DOWN</option>
3452
3674
  <option value="4">FLAP/DOWN/UP</option>
3453
3675
  </select>
3454
- <button id="btnSetRef" title="use the hosts currently shown as the new reference list">SET REFERENCE</button>
3676
+ <button id="btnSetRef" title="use the hosts currently shown as the new reference list - all other hosts are removed and no longer pinged">SET REFERENCE</button>
3455
3677
  <button id="btnZero" title="reset CH-TIME and CH NO for all hosts">ZERO CHANGES</button>
3456
3678
  <button id="btnClear" class="danger" title="remove every host from the list, resets all state">CLEAR ALL</button>
3457
3679
  <select id="selAddrMode" title="Deduplication when the same host is entered as both a hostname and its IP: hostname &amp; ip keeps both as given (no dedup) | prefer hostname: skip a raw IP already covered by a hostname | prefer ip address: skip a hostname already covered by a raw IP | ip only: resolve every hostname to its IP and ping/track it by address">
@@ -3524,8 +3746,8 @@ WEB_INDEX_HTML = r"""<!DOCTYPE html>
3524
3746
  <span class="u">HOSTS-UP: <b id="sUp">0</b></span>
3525
3747
  <span class="d">HOSTS-DOWN: <b id="sDown">0</b></span>
3526
3748
  <span>FLAPPING: <b id="sFlap">0</b></span>
3527
- <span title="hosts matching the display filter, out of the totals above"
3528
- id="sShownWrap">HOSTS-SHOWN: <b id="sShown">0</b></span>
3749
+ <span title="hosts shown by the view and display filters, out of all hosts"
3750
+ id="sShownWrap">SHOWN: <b id="sShown">0</b></span>
3529
3751
  <span id="sLog"></span>
3530
3752
  <span class="msg" id="msg"></span>
3531
3753
  <span id="scrollHint"></span>
@@ -4603,6 +4825,7 @@ function jumpFields(action){
4603
4825
  return {action: action, host: document.getElementById('jumpHost').value, port: document.getElementById('jumpPort').value,
4604
4826
  user: document.getElementById('jumpUser').value, key: document.getElementById('jumpKey').value,
4605
4827
  hostkey: document.getElementById('jumpHostKey').value,
4828
+ resolve: document.getElementById('jumpResolve').checked,
4606
4829
  secret: document.getElementById('jumpSecret').value};
4607
4830
  }
4608
4831
  function jumpButtons(){
@@ -4640,13 +4863,15 @@ function openJump(){
4640
4863
  if(j.ssh && j.ssh.ok){
4641
4864
  pre.style.color = 'var(--dim)';
4642
4865
  pre.textContent = 'ssh client: ' + (j.ssh.version || 'found') + (j.ssh.keyscan ? '' : ' (ssh-keyscan missing: unknown host keys cannot be trusted here)') +
4643
- ' | 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') : '');
4644
4868
  } else {
4645
4869
  pre.style.color = 'var(--down,#f85149)';
4646
4870
  pre.textContent = (j.ssh && j.ssh.hint) || 'OpenSSH client not found on this computer.';
4647
4871
  }
4648
4872
  if(j.last){
4649
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;
4650
4875
  ['host','port','user','key'].forEach(function(k){
4651
4876
  var el = document.getElementById('jump' + k.charAt(0).toUpperCase() + k.slice(1));
4652
4877
  if(el && !el.value) el.value = j.last[k] || '';
@@ -5606,7 +5831,7 @@ function poll(){
5606
5831
  document.getElementById('sFlap').textContent = s.hosts_flap;
5607
5832
  var swrap = document.getElementById('sShownWrap');
5608
5833
  var advOn = !!(s.adv_filter && Object.keys(s.adv_filter).length);
5609
- swrap.style.display = (s.match_filter || advOn) ? '' : 'none';
5834
+ swrap.style.display = ''; // SHOWN is always visible
5610
5835
  lastAdv = {values: s.adv_values || {}, filter: s.adv_filter || {}};
5611
5836
  document.getElementById('btnAdvFilter').className = advOn ? 'on' : '';
5612
5837
  document.getElementById('sShown').textContent = s.hosts_shown;
@@ -7182,6 +7407,8 @@ def build_cli_snapshot(args, backoff, timeout, retries, down_retries, flap_windo
7182
7407
  parts.append('-sshkey ' + args.ssh_key)
7183
7408
  if getattr(args, 'ssh_hostkey', 'accept-new') != 'accept-new':
7184
7409
  parts.append('-sshhk ' + args.ssh_hostkey)
7410
+ if getattr(args, 'ssh_no_resolve', False):
7411
+ parts.append('-sshnr')
7185
7412
  return ' '.join(parts)
7186
7413
 
7187
7414
 
@@ -7372,7 +7599,8 @@ def run_web_mode(original_hosts_list, host_state, args, logfile_file_name,
7372
7599
  gn_thread, gn_result, gn_candidates = get_names_start(
7373
7600
  original_hosts_list, int(args.dns_ttl))
7374
7601
  if gn_thread is None:
7375
- message = 'get names: no eligible IP host(s)'
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)')
7376
7604
  else:
7377
7605
  message = ('get names: running in background (%d host(s))'
7378
7606
  % len(gn_candidates))
@@ -7736,10 +7964,13 @@ def run_web_mode(original_hosts_list, host_state, args, logfile_file_name,
7736
7964
  if cmds or gn_just_finished:
7737
7965
  # view and order are pure display changes - show them at once instead of
7738
7966
  # letting the browser wait for the running round, same as the CLI does
7739
- quick = build_display(view_hosts(filter_mode, active_hosts_list, host_state, tz_offset, flap_window), host_state, sort_mode,
7740
- tz_offset, flap_window)
7741
- quick_up = sum(1 for e in quick if 'UP' in e[1])
7742
- quick_total = len(quick)
7967
+ # HOSTS / HOSTS-UP / HOSTS-DOWN count all hosts, the view and the display
7968
+ # filters only narrow what is SHOWN
7969
+ quick_all = build_display(active_hosts_list, host_state, sort_mode,
7970
+ tz_offset, flap_window)
7971
+ quick_up = sum(1 for e in quick_all if 'UP' in e[1])
7972
+ quick_total = len(quick_all)
7973
+ quick = view_rows(quick_all, filter_mode, host_state, tz_offset, flap_window)
7743
7974
  if match_filter_re is not None or adv_filter:
7744
7975
  quick = apply_match_filter(quick, match_filter_re)
7745
7976
  with web_lock:
@@ -7836,16 +8067,15 @@ def run_web_mode(original_hosts_list, host_state, args, logfile_file_name,
7836
8067
  phase['state'] = time.time() - _t
7837
8068
 
7838
8069
  _t = time.time()
7839
- display_list = build_display(view_hosts(filter_mode, active_hosts_list, host_state, tz_offset, flap_window), host_state, sort_mode,
7840
- tz_offset, flap_window)
7841
- # HOSTS/HOSTS-UP/HOSTS-DOWN are the totals for the whole current view - a
7842
- # [M] display filter narrows what's SHOWN, not what's actually being pinged,
7843
- # so it must not make these look like only 2 hosts exist. hosts_count_shown
7844
- # is the post-filter count, reported separately (HOSTS-SHOWN in the web GUI).
7845
- hosts_count_up = sum(1 for e in display_list if 'UP' in e[1])
7846
- hosts_count_down = len(display_list) - hosts_count_up
7847
- hosts_count_flap = sum(1 for e in display_list
8070
+ display_all = build_display(active_hosts_list, host_state, sort_mode,
8071
+ tz_offset, flap_window)
8072
+ # HOSTS/HOSTS-UP/HOSTS-DOWN/FLAPPING count all hosts - the view and the [M] /
8073
+ # ADV display filters only narrow what is SHOWN (hosts_count_shown below).
8074
+ hosts_count_up = sum(1 for e in display_all if 'UP' in e[1])
8075
+ hosts_count_down = len(display_all) - hosts_count_up
8076
+ hosts_count_flap = sum(1 for e in display_all
7848
8077
  if host_is_flapping(e, now_local(tz_offset), flap_window))
8078
+ display_list = view_rows(display_all, filter_mode, host_state, tz_offset, flap_window)
7849
8079
  if match_filter_re is not None or adv_filter:
7850
8080
  display_list = apply_match_filter(display_list, match_filter_re)
7851
8081
  hosts_count_shown = len(display_list)
@@ -7952,6 +8182,7 @@ if __name__=='__main__':
7952
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)")
7953
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)")
7954
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)")
7955
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)")
7956
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)")
7957
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)")
@@ -8318,7 +8549,7 @@ if __name__=='__main__':
8318
8549
  # (CLI: [A]=ADD HOST, web gui: ADD HOST / UPLOAD FILE)
8319
8550
 
8320
8551
  if args.ssh:
8321
- 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)
8322
8553
  elif args.ssh_key:
8323
8554
  error_handler("ERROR: -sshkey needs -ssh")
8324
8555
 
@@ -8566,13 +8797,15 @@ if __name__=='__main__':
8566
8797
  Same actions as the web gui window (jump_handle). Pinging keeps running meanwhile."""
8567
8798
  rows, cols = screen.getmaxyx()
8568
8799
  dw = min(76, max(40, cols - 4))
8569
- dh = 18
8800
+ dh = 19
8570
8801
  dy = max(0, rows // 2 - dh // 2)
8571
8802
  dx = max(0, cols // 2 - dw // 2)
8572
- labels = ['HOST', 'PORT', 'USER', 'PASSWORD', 'HOST KEY', 'KEY FILE']
8573
- 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']
8574
8805
  vals = [_jump_last.get('host', ''), _jump_last.get('port', ''), _jump_last.get('user', ''),
8575
- '', _jump_last.get('hostkey', 'accept-new'), _jump_last.get('key', '')]
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'}
8576
8809
  hk_text = {'ask': 'ask (confirm unknown key)', 'accept-new': 'accept new hosts', 'off': 'OFF - no check (insecure)'}
8577
8810
  pre = ssh_prereq()
8578
8811
  status = ['']
@@ -8618,11 +8851,13 @@ if __name__=='__main__':
8618
8851
  screen_output(dy + 3, dx, '│' + (' ' + info)[:dw - 2].ljust(dw - 2) + '│', cls, 0)
8619
8852
  fw = dw - 16
8620
8853
  for i, lab in enumerate(labels):
8621
- shown = ('*' * len(vals[i])) if keys[i] == 'secret' else (hk_text[vals[i]] + ' [ENTER/SPACE=change]' if keys[i] == 'hostkey' else vals[i])
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])
8622
8857
  shown = shown[-fw:] if len(shown) > fw else shown
8623
8858
  line = ' ' + lab.ljust(10) + (shown + '_' * 0).ljust(fw) + ' '
8624
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)
8625
- screen_output(dy + 10, dx, '│' + ' ' * (dw - 2) + '│', 1, 0)
8860
+ screen_output(dy + 11, dx, '│' + ' ' * (dw - 2) + '│', 1, 0)
8626
8861
  lines = []
8627
8862
  for t in status:
8628
8863
  lines.extend(wrap(t, dw - 4))
@@ -8630,18 +8865,18 @@ if __name__=='__main__':
8630
8865
  lines.extend(wrap(f, dw - 4))
8631
8866
  for i in range(4):
8632
8867
  txt = lines[i] if i < len(lines) else ''
8633
- screen_output(dy + 11 + i, dx, '│' + (' ' + txt)[:dw - 2].ljust(dw - 2) + '│', status_cls, 0)
8868
+ screen_output(dy + 12 + i, dx, '│' + (' ' + txt)[:dw - 2].ljust(dw - 2) + '│', status_cls, 0)
8634
8869
  bl = buttons()
8635
8870
  col = dx + 2
8636
- screen_output(dy + 15, dx, '│' + ' ' * (dw - 2) + '│', 1, 0)
8871
+ screen_output(dy + 16, dx, '│' + ' ' * (dw - 2) + '│', 1, 0)
8637
8872
  for i, (name, _a) in enumerate(bl):
8638
8873
  lab = ' [' + name + '] '
8639
8874
  hi = (focus == len(labels) + i)
8640
- screen_output(dy + 15, col, lab, 2 if hi else 1, 1 if hi else 0)
8875
+ screen_output(dy + 16, col, lab, 2 if hi else 1, 1 if hi else 0)
8641
8876
  col += len(lab) + 1
8642
- screen_output(dy + 16, dx, '│' + ' TAB/arrows=move ENTER=select ESC=close'.ljust(dw - 2) + '│', 1, 0)
8643
- screen_output(dy + 17, dx, '└' + '─' * (dw - 2) + '┘', 1, 1)
8644
- if focus < len(labels) and keys[focus] != 'hostkey' and not busy['t']:
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']:
8645
8880
  curses.curs_set(1)
8646
8881
  shown_len = min(len(vals[focus]), fw)
8647
8882
  screen.move(dy + 4 + focus, dx + 12 + shown_len)
@@ -8713,6 +8948,8 @@ if __name__=='__main__':
8713
8948
  focus = (focus - 1) % nfocus
8714
8949
  elif ch in (10, 13, 32) and focus < len(labels) and keys[focus] == 'hostkey':
8715
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'
8716
8953
  elif ch in (10, 13):
8717
8954
  if focus < len(labels):
8718
8955
  focus = (focus + 1) % nfocus
@@ -8724,7 +8961,7 @@ if __name__=='__main__':
8724
8961
  status, status_cls = [pre['hint']], 3
8725
8962
  else:
8726
8963
  start(act)
8727
- elif focus < len(labels):
8964
+ elif focus < len(labels) and keys[focus] not in ('hostkey', 'resolve'):
8728
8965
  if ch in (curses.KEY_BACKSPACE, 127, 8):
8729
8966
  vals[focus] = vals[focus][:-1]
8730
8967
  elif 32 <= ch <= 126 and len(vals[focus]) < 255:
@@ -9520,7 +9757,8 @@ if __name__=='__main__':
9520
9757
  gn_thread, gn_result, gn_candidates = get_names_start(
9521
9758
  original_hosts_list, int(args.dns_ttl))
9522
9759
  if gn_thread is None:
9523
- notice('GET NAMES: NO ELIGIBLE IP HOST(S)', 2, 3)
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)
9524
9762
  message = 'get names: no eligible ip host(s)'
9525
9763
  else:
9526
9764
  notice('GET NAMES: RUNNING IN BACKGROUND (%d HOST(S))'
@@ -9773,6 +10011,7 @@ if __name__=='__main__':
9773
10011
  hosts_count_up = 0
9774
10012
  hosts_count_down = 0
9775
10013
  hosts_count_flap = 0
10014
+ hosts_count_shown = 0
9776
10015
  run_time = '0.00'
9777
10016
  phase = {}
9778
10017
  used_scan = ''
@@ -9826,7 +10065,7 @@ if __name__=='__main__':
9826
10065
  # mirror the same fix as run_web_mode(): HOSTS-UP/DOWN in the web view should
9827
10066
  # be the totals for the current view, not narrowed by the [M] display filter
9828
10067
  # the way the curses screen's own hosts_count_up/down are (left untouched here).
9829
- total_list = build_display(view_hosts(filter_mode, active_hosts_list, host_state, tz_offset, flap_window), host_state, sort_mode,
10068
+ total_list = build_display(active_hosts_list, host_state, sort_mode,
9830
10069
  tz_offset, flap_window)
9831
10070
  total_up = sum(1 for e in total_list if 'UP' in e[1])
9832
10071
  total_down = len(total_list) - total_up
@@ -9855,17 +10094,20 @@ if __name__=='__main__':
9855
10094
  so they can take effect at once instead of after the next scan round.
9856
10095
  """
9857
10096
  global display_list, hosts_count_up, hosts_count_down, hosts_count_flap
9858
- display_list = build_display(view_hosts(filter_mode, active_hosts_list, host_state, tz_offset, flap_window), host_state, sort_mode,
9859
- tz_offset, flap_window)
9860
- # HOSTS/UP/DOWN/FLAPPING are the totals of the whole view - [M] and ADV FILTER
9861
- # only narrow what is shown, every host is still pinged
10097
+ global hosts_count_shown
10098
+ display_all = build_display(active_hosts_list, host_state, sort_mode,
10099
+ tz_offset, flap_window)
10100
+ # HOSTS/UP/DOWN/FLAPPING count all hosts - the view, [M] and ADV FILTER only
10101
+ # narrow what is shown (SHOWN), every host is still pinged
9862
10102
  _now_ref = now_local(tz_offset)
9863
- hosts_count_up = sum(1 for e in display_list if 'UP' in e[1])
9864
- hosts_count_down = len(display_list) - hosts_count_up
9865
- hosts_count_flap = sum(1 for e in display_list
10103
+ hosts_count_up = sum(1 for e in display_all if 'UP' in e[1])
10104
+ hosts_count_down = len(display_all) - hosts_count_up
10105
+ hosts_count_flap = sum(1 for e in display_all
9866
10106
  if host_is_flapping(e, _now_ref, flap_window))
10107
+ display_list = view_rows(display_all, filter_mode, host_state, tz_offset, flap_window)
9867
10108
  if match_filter_re is not None or adv_filter:
9868
10109
  display_list = apply_match_filter(display_list, match_filter_re)
10110
+ hosts_count_shown = len(display_list)
9869
10111
 
9870
10112
  def draw_screen():
9871
10113
  """Paint the whole screen from the data of the last completed round.
@@ -9985,11 +10227,13 @@ if __name__=='__main__':
9985
10227
  screen_output(rows - 1, 67, 'DOWN: ', 1, 1)
9986
10228
  screen_output(rows - 1, 73, hosts_down, 3, 1)
9987
10229
  screen_output(rows - 1, 79, 'FLAPPING: ', 1, 1)
9988
- screen_output(rows - 1, 89, hosts_flap.rstrip() + ' |', 1, 1)
10230
+ screen_output(rows - 1, 89, hosts_flap.rstrip(), 1, 1)
10231
+ screen_output(rows - 1, 96, 'SHOWN: ', 1, 1)
10232
+ screen_output(rows - 1, 103, ('{m: <6}'.format(m=hosts_count_shown)).rstrip() + ' |', 1, 1)
9989
10233
  if args.disable_logging:
9990
- screen_output(rows - 1, 99, 'LOGGING-ON: ' + logfile_file_name, 1, 1)
10234
+ screen_output(rows - 1, 113, 'LOGGING-ON: ' + logfile_file_name, 1, 1)
9991
10235
  else:
9992
- screen_output(rows - 1, 99, 'LOGGING-OFF', 1, 1)
10236
+ screen_output(rows - 1, 113, 'LOGGING-OFF', 1, 1)
9993
10237
  if num_of_hosts > 0 and maxhosts < num_of_hosts:
9994
10238
  tts_text = ' | TERMINAL TOO SMALL '
9995
10239
  screen_output(rows - 1, cols - len(tts_text), tts_text, 3, 2)
@@ -10292,7 +10536,8 @@ if __name__=='__main__':
10292
10536
  gn_thread, gn_result, gn_candidates = get_names_start(
10293
10537
  original_hosts_list, int(args.dns_ttl))
10294
10538
  if gn_thread is None:
10295
- notice('GET NAMES: NO ELIGIBLE IP HOST(S)', 2, 3)
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)
10296
10541
  else:
10297
10542
  notice('GET NAMES: RUNNING IN BACKGROUND (%d HOST(S))'
10298
10543
  % len(gn_candidates), 2)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eping
3
- Version: 4.1
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.01
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.
@@ -235,8 +235,8 @@ Single self-contained page, no external resources.
235
235
 
236
236
  - Table: CLI-style columns, filled top-down then next column right. Horizontal scroll
237
237
  if needed ("SCROLL RIGHT FOR MORE" hint in the stats bar).
238
- - Footer stats: HOSTS, RUNTIME, HOST UP/DOWN/FLAPPING, for the whole current view regardless
239
- of the match filter and the ADV FILTER (HOSTS-SHOWN = shown count). The CLI footer counts the same way. RUNTIME shows `n/a` briefly during a background round.
238
+ - Footer stats: HOSTS, RUNTIME, HOST UP/DOWN/FLAPPING always count all hosts; SHOWN is the
239
+ number of rows displayed after the view, the match filter and the ADV FILTER. The CLI footer counts the same way. RUNTIME shows `n/a` briefly during a background round.
240
240
  - Adding many hosts (`ADD`, ADD HOSTS FILE, upload) shows "added N host(s) - please wait,
241
241
  pinging ..." until they appear with a state, then `done`.
242
242
  - Font size 6–28px (slider, `A−`/`A+`, `+`/`−` keys), stored in `localStorage`.
@@ -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
  ![JUMPHOST window](https://raw.githubusercontent.com/ewaldj/eping/main/docs/img/jumphost.png)
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: GET NAMES and the CSV-IP fallback are not available over ssh; at most 4 ssh processes
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes