eping 3.86__tar.gz → 3.91__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: 3.86
3
+ Version: 3.91
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 3.86
20
+ # eping.py 3.91
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.
@@ -244,7 +244,7 @@ Single self-contained page, no external resources.
244
244
  - Row 1: view select (9 views), sort order select, SET REFERENCE, ZERO CHANGES,
245
245
  CLEAR ALL, address mode select, GET NAMES, ADV OPTIONS, RESET LOG, FILE OPERATIONS
246
246
  select, EXIT, font size.
247
- - Row 2: match filter (SET/CLEAR), ADV FILTER, host field (ADD/DELETE), comment
247
+ - Row 2: match filter (SET/CLEAR), ADV FILTER, host field (ADD/DELETE), ADD/DEL LIST, comment
248
248
  field (COMMENT), GENERATE REPORT select.
249
249
  - View select, sort order select, address mode select and ADV OPTIONS all match the
250
250
  CLI's `U`/`O`/`P`/`X` pickers exactly. FILE OPERATIONS (incl. ADD HOSTS FILE FROM CLIENT/
@@ -273,6 +273,13 @@ Single self-contained page, no external resources.
273
273
  `+`/`−` font size (works while typing). No keyboard shortcut for `X`/ADV OPTIONS or
274
274
  GENERATE REPORT - click required.
275
275
  - Host field feeds both ADD/DELETE; ENTER triggers the button used last, ESC clears.
276
+ - **ADD/DEL LIST** (button after DELETE; also Ctrl/Cmd+V on the page while no field has focus): paste a
277
+ host list - one per line or separated by comma/semicolon/blank, `#` comment, CIDR expanded. A live
278
+ preview shows found / new / already in list / ignored words / masks out of range; ADD and DELETE are
279
+ enabled only if they would change something (DELETE over 20 hosts asks to confirm; exact name match
280
+ only). A pasted inventory `.csv` (with the `IP Address`/`Hostname` header) also updates hosts already in
281
+ the list: IP entries (e.g. from a `.txt`) are renamed to the CSV hostname, history kept, and the CSV
282
+ values are attached. Works with `-wvc` too.
276
283
  - Files can also be dropped anywhere on the page (max 16MB, see *Host file format*).
277
284
  - Commands acknowledge immediately; a click aborts the running round.
278
285
  - **Host popup**: hovering a host row (after ~350 ms) shows everything known about it:
@@ -1,4 +1,4 @@
1
- # eping.py 3.86
1
+ # eping.py 3.91
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.
@@ -225,7 +225,7 @@ Single self-contained page, no external resources.
225
225
  - Row 1: view select (9 views), sort order select, SET REFERENCE, ZERO CHANGES,
226
226
  CLEAR ALL, address mode select, GET NAMES, ADV OPTIONS, RESET LOG, FILE OPERATIONS
227
227
  select, EXIT, font size.
228
- - Row 2: match filter (SET/CLEAR), ADV FILTER, host field (ADD/DELETE), comment
228
+ - Row 2: match filter (SET/CLEAR), ADV FILTER, host field (ADD/DELETE), ADD/DEL LIST, comment
229
229
  field (COMMENT), GENERATE REPORT select.
230
230
  - View select, sort order select, address mode select and ADV OPTIONS all match the
231
231
  CLI's `U`/`O`/`P`/`X` pickers exactly. FILE OPERATIONS (incl. ADD HOSTS FILE FROM CLIENT/
@@ -254,6 +254,13 @@ Single self-contained page, no external resources.
254
254
  `+`/`−` font size (works while typing). No keyboard shortcut for `X`/ADV OPTIONS or
255
255
  GENERATE REPORT - click required.
256
256
  - Host field feeds both ADD/DELETE; ENTER triggers the button used last, ESC clears.
257
+ - **ADD/DEL LIST** (button after DELETE; also Ctrl/Cmd+V on the page while no field has focus): paste a
258
+ host list - one per line or separated by comma/semicolon/blank, `#` comment, CIDR expanded. A live
259
+ preview shows found / new / already in list / ignored words / masks out of range; ADD and DELETE are
260
+ enabled only if they would change something (DELETE over 20 hosts asks to confirm; exact name match
261
+ only). A pasted inventory `.csv` (with the `IP Address`/`Hostname` header) also updates hosts already in
262
+ the list: IP entries (e.g. from a `.txt`) are renamed to the CSV hostname, history kept, and the CSV
263
+ values are attached. Works with `-wvc` too.
257
264
  - Files can also be dropped anywhere on the page (max 16MB, see *Host file format*).
258
265
  - Commands acknowledge immediately; a click aborts the running round.
259
266
  - **Host popup**: hovering a host row (after ~350 ms) shows everything known about it:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "eping"
7
- version = "3.86"
7
+ version = "3.91"
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 = '3.86'
10
+ VERSION = '3.91'
11
11
  version = VERSION # legacy alias (kept for existing references)
12
12
 
13
13
  # --- scaling limits ---
@@ -710,6 +710,26 @@ def _rename_host_in_place(old, new, original_hosts_list, active_hosts_list,
710
710
  host_csv_ip.setdefault(new, host_csv_ip[old])
711
711
  forget_names([new]) # force a fresh forward lookup for the new identity
712
712
 
713
+ def merge_csv_ips_into_lists(hosts, original_hosts_list, active_hosts_list,
714
+ host_state, up_seen, down_streak):
715
+ """ADD LIST with an inventory CSV: a CSV host whose IP is already in the lists as a
716
+ plain IP entry (e.g. from a .txt file) updates that entry - it is renamed to the
717
+ CSV hostname in place (history kept) instead of being added a second time.
718
+ Returns the number of renamed entries. host_csv_ip holds the IP of each CSV host."""
719
+ cur = set(original_hosts_list) | set(active_hosts_list)
720
+ renamed = 0
721
+ for h in hosts:
722
+ if h in cur:
723
+ continue
724
+ ip = host_csv_ip.get(h)
725
+ if ip and ip != h and ip in cur:
726
+ _rename_host_in_place(ip, h, original_hosts_list, active_hosts_list,
727
+ host_state, up_seen, down_streak)
728
+ cur.discard(ip)
729
+ cur.add(h)
730
+ renamed += 1
731
+ return renamed
732
+
713
733
  def _forward_confirms(name, ip):
714
734
  """True if 'name' has a forward record (A for an IPv4 ip, AAAA for an IPv6 ip)
715
735
  that resolves back to exactly this ip. Guards GET NAMES: a PTR record with no
@@ -1768,7 +1788,7 @@ def parse_hosts_from_text(text, stats=None, use_names=False, name=None):
1768
1788
  recognised by its header; anything else - and every file named *.txt - is parsed
1769
1789
  as a plain host list (_parse_plain_hosts). A NUL byte separates concatenated files."""
1770
1790
  csv_ok = not (name and str(name).lower().endswith('.txt'))
1771
- out, seen, networks, skipped = [], set(), 0, []
1791
+ out, seen, networks, skipped, ignored = [], set(), 0, [], []
1772
1792
  csv_stats = {}
1773
1793
  for seg in (text or '').split('\x00'):
1774
1794
  sub = {}
@@ -1781,6 +1801,7 @@ def parse_hosts_from_text(text, stats=None, use_names=False, name=None):
1781
1801
  csv_stats['csv'] = True
1782
1802
  networks += sub.get('networks', 0)
1783
1803
  skipped.extend(sub.get('skipped', []))
1804
+ ignored.extend(sub.get('ignored', []))
1784
1805
  for h in got:
1785
1806
  if h not in seen:
1786
1807
  seen.add(h)
@@ -1788,6 +1809,7 @@ def parse_hosts_from_text(text, stats=None, use_names=False, name=None):
1788
1809
  if stats is not None:
1789
1810
  stats['networks'] = networks
1790
1811
  stats['skipped'] = skipped
1812
+ stats['ignored'] = ignored
1791
1813
  stats.update(csv_stats)
1792
1814
  return out
1793
1815
 
@@ -1804,6 +1826,7 @@ def _parse_plain_hosts(text, stats=None):
1804
1826
  fqdns = []
1805
1827
  networks = 0
1806
1828
  skipped = []
1829
+ ignored = []
1807
1830
  for line in (text or '').splitlines():
1808
1831
  line = line.split('#')[0] # ignore comments
1809
1832
  for word in line.replace(',', ' ').replace(';', ' ').split():
@@ -1823,6 +1846,8 @@ def _parse_plain_hosts(text, stats=None):
1823
1846
  ips.append(normalize_ip(word.rsplit('/', 1)[0])) # single-host IPv6 CIDR, no expansion
1824
1847
  elif match_re(word, fqdn_re):
1825
1848
  fqdns.append(word)
1849
+ else:
1850
+ ignored.append(word)
1826
1851
  seen = set()
1827
1852
  out = []
1828
1853
  for h in ips + fqdns:
@@ -1832,6 +1857,7 @@ def _parse_plain_hosts(text, stats=None):
1832
1857
  if stats is not None:
1833
1858
  stats['networks'] = networks
1834
1859
  stats['skipped'] = skipped
1860
+ stats['ignored'] = ignored
1835
1861
  return out
1836
1862
 
1837
1863
  def extract_opt_lines(text):
@@ -2848,6 +2874,19 @@ WEB_INDEX_HTML = r"""<!DOCTYPE html>
2848
2874
  </div>
2849
2875
  </div>
2850
2876
  </div>
2877
+ <div id="hostListModal" class="modal-overlay" style="display:none">
2878
+ <div class="modal-box wide">
2879
+ <h3>ADD / DELETE HOST LIST</h3>
2880
+ <p>Paste hosts: IPv4/IPv6, hostname, IPv4 CIDR - separated by new line, comma, semicolon or blank; `#` starts a comment.</p>
2881
+ <textarea id="hostListText" rows="10" spellcheck="false" style="width:100%;box-sizing:border-box;margin-bottom:8px;background:var(--panel);color:var(--fg);border:1px solid var(--ctrl-line);border-radius:4px;padding:6px 8px;font-family:inherit;font-size:12px;resize:vertical"></textarea>
2882
+ <div id="hostListInfo" style="min-height:34px;margin-bottom:12px;font-size:12px;color:var(--dim)">&nbsp;</div>
2883
+ <div class="modal-buttons">
2884
+ <button id="hostListAddBtn" disabled>ADD</button>
2885
+ <button id="hostListDelBtn" class="danger" disabled>DELETE</button>
2886
+ <button id="hostListCancelBtn">CANCEL</button>
2887
+ </div>
2888
+ </div>
2889
+ </div>
2851
2890
  <div id="addFromClientModal" class="modal-overlay" style="display:none">
2852
2891
  <div class="modal-box">
2853
2892
  <h3>ADD FROM CLIENT</h3>
@@ -3006,6 +3045,7 @@ WEB_INDEX_HTML = r"""<!DOCTYPE html>
3006
3045
  <input type="text" id="addInput" placeholder="IPv4/IPv6, host, IPv4 CIDR, IPv6 /128, ip1-ip2">
3007
3046
  <button id="btnAdd" title="add the given host(s) to the list - same input as DELETE">ADD</button>
3008
3047
  <button id="btnDel" title="remove the given host(s) - same input as ADD">DELETE</button>
3048
+ <button id="btnList" title="paste a list of hosts (one per line, or separated by comma/semicolon/blank) to add or delete - also opened by Ctrl/Cmd+V on the page">ADD/DEL LIST</button>
3009
3049
  </span>
3010
3050
  <span class="sep">&nbsp;|&nbsp;</span>
3011
3051
  <span class="grp">
@@ -4095,6 +4135,68 @@ document.getElementById('commentInput').addEventListener('keydown', function(e){
4095
4135
  document.getElementById('btnClear').onclick = function(){
4096
4136
  if(confirm('Remove ALL hosts and reset their state?')){ post('clear'); } };
4097
4137
 
4138
+ /* ---- ADD/DEL LIST window (paste a host list), preview before applying ---- */
4139
+ var hostListModal = document.getElementById('hostListModal');
4140
+ var hostListText = document.getElementById('hostListText');
4141
+ var hostListInfo = document.getElementById('hostListInfo');
4142
+ var hostListAddBtn = document.getElementById('hostListAddBtn');
4143
+ var hostListDelBtn = document.getElementById('hostListDelBtn');
4144
+ var hostListTimer = null, hostListSeq = 0, hostListInList = 0, hostListNew = 0;
4145
+ function hostListOpen(){ return hostListModal.style.display === 'flex'; }
4146
+ function closeHostList(){ hostListModal.style.display = 'none'; hostListText.value = ''; }
4147
+ function openHostList(text){
4148
+ hostListText.value = text || '';
4149
+ hostListModal.style.display = 'flex';
4150
+ hostListPreviewNow();
4151
+ hostListText.focus();
4152
+ }
4153
+ function hostListPost(action){
4154
+ return fetch('api/hostlist', {method:'POST', headers:{'Content-Type':'application/json'},
4155
+ body: JSON.stringify({text: hostListText.value, action: action})}).then(function(r){ return r.json(); });
4156
+ }
4157
+ function hostListPreviewNow(){
4158
+ var seq = ++hostListSeq;
4159
+ if(!hostListText.value.trim()){
4160
+ hostListInfo.innerHTML = '&nbsp;'; hostListAddBtn.disabled = true; hostListDelBtn.disabled = true; return;
4161
+ }
4162
+ hostListPost('preview').then(function(j){
4163
+ if(seq !== hostListSeq || !j.ok) return;
4164
+ hostListInList = j.in_list; hostListNew = j.new;
4165
+ var t = j.found + ' host(s) found: ' + j.new + ' new (ADD), ' + j.in_list + ' already in list (DELETE)';
4166
+ if(j.csv) t += ' - CSV: existing hosts are updated with its data' + (j.renamed ? ' (' + j.renamed + ' IP entries renamed to hostname)' : '');
4167
+ if(j.networks) t += ' - ' + j.networks + ' network(s) expanded';
4168
+ if(j.ignored) t += ' - ' + j.ignored + ' ignored: ' + j.ignored_sample.join(', ') + (j.ignored > j.ignored_sample.length ? ' ...' : '');
4169
+ if(j.skipped.length) t += ' - mask out of range: ' + j.skipped.join(', ');
4170
+ hostListInfo.textContent = t;
4171
+ hostListAddBtn.disabled = !(j.new || (j.csv && (j.in_list || j.renamed)));
4172
+ hostListDelBtn.disabled = !j.in_list;
4173
+ }).catch(function(){});
4174
+ }
4175
+ hostListText.addEventListener('input', function(){
4176
+ clearTimeout(hostListTimer); hostListTimer = setTimeout(hostListPreviewNow, 300); });
4177
+ function hostListRun(action){
4178
+ note(action === 'add' ? 'adding host list ...' : 'removing host list ...', true);
4179
+ hostListPost(action).then(function(j){ if(!j.ok){ note('list failed: ' + (j.error||''), false); } })
4180
+ .catch(function(){ note('list failed', false); });
4181
+ closeHostList();
4182
+ }
4183
+ hostListAddBtn.onclick = function(){ hostListRun('add'); };
4184
+ hostListDelBtn.onclick = function(){
4185
+ if(hostListInList > 20 && !confirm('Remove ' + hostListInList + ' hosts from the list?')) return;
4186
+ hostListRun('del'); };
4187
+ document.getElementById('hostListCancelBtn').onclick = closeHostList;
4188
+ document.getElementById('btnList').onclick = function(){ openHostList(''); };
4189
+ /* Ctrl/Cmd+V with no field focused and no window open: open the window with the clipboard text */
4190
+ document.addEventListener('paste', function(e){
4191
+ var a = document.activeElement, tn = a && a.tagName;
4192
+ if(isReadOnly || hostListOpen() || tn === 'INPUT' || tn === 'TEXTAREA' || tn === 'SELECT') return;
4193
+ if(document.querySelector('.modal-overlay[style*="flex"]')) return;
4194
+ var t = e.clipboardData && e.clipboardData.getData('text');
4195
+ if(!t || !t.trim()) return;
4196
+ e.preventDefault();
4197
+ openHostList(t);
4198
+ });
4199
+
4098
4200
  /* ---- host file upload ---- */
4099
4201
  var fileInput = document.getElementById('fileInput');
4100
4202
  var fileInputVis = document.getElementById('fileInputVis');
@@ -4511,6 +4613,10 @@ document.addEventListener('keydown', function(e){
4511
4613
  if(sk === 'y' || e.key === 'Enter'){ runSaveHostsFile(); e.preventDefault(); }
4512
4614
  return;
4513
4615
  }
4616
+ if(hostListOpen()){
4617
+ if(e.key === 'Escape'){ closeHostList(); e.preventDefault(); }
4618
+ return;
4619
+ }
4514
4620
  if(addFromClientOpen()){
4515
4621
  if(e.key === 'Escape'){ closeAddFromClient(); e.preventDefault(); }
4516
4622
  return;
@@ -5802,6 +5908,45 @@ class EpingWebHandler(http.server.BaseHTTPRequestHandler):
5802
5908
  self._respond(200, 'application/json; charset=utf-8', json.dumps({'ok': True}))
5803
5909
  return
5804
5910
 
5911
+ # --- ADD/DEL LIST window: preview (no change) or queue add_list/del_list ---
5912
+ if path in ('/api/hostlist', 'api/hostlist'):
5913
+ raw = self._read_body(WEB_MAX_UPLOAD)
5914
+ if raw is None:
5915
+ self._respond(413, 'application/json; charset=utf-8',
5916
+ json.dumps({'ok': False, 'error': 'text too large'}))
5917
+ return
5918
+ try:
5919
+ req = json.loads(raw.decode('utf-8', 'replace') or '{}')
5920
+ except Exception:
5921
+ req = {}
5922
+ text = str(req.get('text', ''))
5923
+ action = str(req.get('action', 'preview'))
5924
+ if action in ('add', 'del'):
5925
+ if not self._queue_command((action + '_list', text)):
5926
+ return
5927
+ self._respond(200, 'application/json; charset=utf-8', json.dumps({'ok': True}))
5928
+ return
5929
+ pstats = {}
5930
+ _m0, _c0 = set(host_meta), set(host_csv_ip)
5931
+ found = parse_hosts_from_text(text, pstats, True)
5932
+ with web_lock:
5933
+ present = set(web_state.get('host_list_all') or [])
5934
+ n_ren = (sum(1 for h in found if h not in present and host_csv_ip.get(h) in present
5935
+ and host_csv_ip.get(h) != h) if pstats.get('csv') else 0)
5936
+ for _k in set(host_meta) - _m0: # preview only - keep no CSV data of it
5937
+ host_meta.pop(_k, None)
5938
+ for _k in set(host_csv_ip) - _c0:
5939
+ host_csv_ip.pop(_k, None)
5940
+ n_in = sum(1 for h in found if h in present)
5941
+ self._respond(200, 'application/json; charset=utf-8', json.dumps({
5942
+ 'ok': True, 'found': len(found), 'in_list': n_in,
5943
+ 'csv': bool(pstats.get('csv')), 'renamed': n_ren,
5944
+ 'new': len(found) - n_in - n_ren, 'networks': pstats.get('networks', 0),
5945
+ 'ignored': len(pstats.get('ignored', [])),
5946
+ 'ignored_sample': pstats.get('ignored', [])[:8],
5947
+ 'skipped': pstats.get('skipped', [])[:5]}))
5948
+ return
5949
+
5805
5950
  # --- FILE ACTIONS > UPLOAD: save a *.txt/*.csv file as-is, no host
5806
5951
  # parsing (see /api/upload above for that) ---
5807
5952
  if path in ('/api/upload_server_file', 'api/upload_server_file'):
@@ -6658,6 +6803,47 @@ def run_web_mode(original_hosts_list, host_state, args, logfile_file_name,
6658
6803
  message += ' - please wait, pinging ...'
6659
6804
  write_log_info(args.disable_logging, logfile_file_name,
6660
6805
  'ADD -f (uploaded, ' + str(added) + ' host(s))', tz_offset)
6806
+ elif cmd in ('add_list', 'del_list'):
6807
+ lst_stats = {}
6808
+ lst_hosts = parse_hosts_from_text(value, lst_stats, addr_mode not in (2, 3))
6809
+ if not lst_hosts:
6810
+ message = ('list: mask must be /%d../%d: %s'
6811
+ % (CIDR_MIN_MASK, CIDR_MAX_MASK, lst_stats['skipped'][0])
6812
+ if lst_stats.get('skipped') else 'list: no valid host found')
6813
+ elif cmd == 'add_list':
6814
+ renamed = (merge_csv_ips_into_lists(lst_hosts, original_hosts_list,
6815
+ active_hosts_list, host_state,
6816
+ up_seen, down_streak)
6817
+ if lst_stats.get('csv') else 0)
6818
+ before_active = set(active_hosts_list)
6819
+ added, err = add_hosts_to(lst_hosts, active_hosts_list, original_hosts_list)
6820
+ if err:
6821
+ message = 'list ' + err
6822
+ else:
6823
+ message = ('list: ' + str(added) + ' new host(s) of '
6824
+ + str(len(lst_hosts)) + ' found')
6825
+ if lst_stats.get('csv'):
6826
+ message += (', %d existing host(s) updated (%d renamed to hostname)'
6827
+ % (len(lst_hosts) - added, renamed))
6828
+ if lst_stats.get('ignored'):
6829
+ message += ', %d ignored' % len(lst_stats['ignored'])
6830
+ if added:
6831
+ pending_add_hosts |= (set(lst_hosts) - before_active)
6832
+ message += ' - please wait, pinging ...'
6833
+ write_log_info(args.disable_logging, logfile_file_name,
6834
+ 'ADD list (' + str(added) + ' host(s))', tz_offset)
6835
+ else:
6836
+ removed = remove_hosts_from(lst_hosts, active_hosts_list,
6837
+ original_hosts_list, host_state)
6838
+ up_seen.difference_update(lst_hosts)
6839
+ for _h in lst_hosts:
6840
+ down_streak.pop(_h, None)
6841
+ forget_names(lst_hosts)
6842
+ message = ('list: removed ' + str(removed) + ' of '
6843
+ + str(len(lst_hosts)) + ' host(s) found')
6844
+ if removed:
6845
+ write_log_info(args.disable_logging, logfile_file_name,
6846
+ 'DEL list (' + str(removed) + ' host(s))', tz_offset)
6661
6847
  elif cmd == 'set_ref':
6662
6848
  # what is actually shown becomes the new reference list - the view
6663
6849
  # filter (active_hosts_list) AND the [M]/display filter (display_list
@@ -8357,6 +8543,52 @@ if __name__=='__main__':
8357
8543
  write_log_info(args.disable_logging, logfile_file_name,
8358
8544
  host_spec_cli('DEL', bval), tz_offset)
8359
8545
 
8546
+ elif bcmd in ('add_list', 'del_list'):
8547
+ lst_stats = {}
8548
+ lst_hosts = parse_hosts_from_text(bval, lst_stats, not (prefer_ip or ip_only_mode))
8549
+ if not lst_hosts:
8550
+ message = ('list: mask must be /%d..%d: %s'
8551
+ % (CIDR_MIN_MASK, CIDR_MAX_MASK, lst_stats['skipped'][0])
8552
+ if lst_stats.get('skipped') else 'list: no valid host found')
8553
+ notice(message.upper(), 3)
8554
+ elif bcmd == 'add_list':
8555
+ renamed = (merge_csv_ips_into_lists(lst_hosts, original_hosts_list,
8556
+ active_hosts_list, host_state,
8557
+ up_seen, down_streak)
8558
+ if lst_stats.get('csv') else 0)
8559
+ before_active = set(active_hosts_list)
8560
+ added, err = add_hosts_to(lst_hosts, active_hosts_list, original_hosts_list)
8561
+ if err:
8562
+ notice(('LIST ' + err).upper(), 3)
8563
+ message = 'list ' + err
8564
+ else:
8565
+ message = ('list: ' + str(added) + ' new host(s) of '
8566
+ + str(len(lst_hosts)) + ' found')
8567
+ if lst_stats.get('csv'):
8568
+ message += (', %d existing host(s) updated (%d renamed to hostname)'
8569
+ % (len(lst_hosts) - added, renamed))
8570
+ if lst_stats.get('ignored'):
8571
+ message += ', %d ignored' % len(lst_stats['ignored'])
8572
+ notice(message.upper(), 2)
8573
+ if added:
8574
+ pending_add_hosts |= (set(lst_hosts) - before_active)
8575
+ message += ' - please wait, pinging ...'
8576
+ write_log_info(args.disable_logging, logfile_file_name,
8577
+ 'ADD list (' + str(added) + ' host(s))', tz_offset)
8578
+ else:
8579
+ removed = remove_hosts_from(lst_hosts, active_hosts_list,
8580
+ original_hosts_list, host_state)
8581
+ up_seen.difference_update(lst_hosts)
8582
+ for _h in lst_hosts:
8583
+ down_streak.pop(_h, None)
8584
+ forget_names(lst_hosts)
8585
+ message = ('list: removed ' + str(removed) + ' of '
8586
+ + str(len(lst_hosts)) + ' host(s) found')
8587
+ notice(message.upper(), 2 if removed else 3)
8588
+ if removed:
8589
+ write_log_info(args.disable_logging, logfile_file_name,
8590
+ 'DEL list (' + str(removed) + ' host(s))', tz_offset)
8591
+
8360
8592
  elif bcmd == 'upload':
8361
8593
  up_stats = {}
8362
8594
  new_hosts = parse_hosts_from_text(bval, up_stats, not (prefer_ip or ip_only_mode))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eping
3
- Version: 3.86
3
+ Version: 3.91
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 3.86
20
+ # eping.py 3.91
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.
@@ -244,7 +244,7 @@ Single self-contained page, no external resources.
244
244
  - Row 1: view select (9 views), sort order select, SET REFERENCE, ZERO CHANGES,
245
245
  CLEAR ALL, address mode select, GET NAMES, ADV OPTIONS, RESET LOG, FILE OPERATIONS
246
246
  select, EXIT, font size.
247
- - Row 2: match filter (SET/CLEAR), ADV FILTER, host field (ADD/DELETE), comment
247
+ - Row 2: match filter (SET/CLEAR), ADV FILTER, host field (ADD/DELETE), ADD/DEL LIST, comment
248
248
  field (COMMENT), GENERATE REPORT select.
249
249
  - View select, sort order select, address mode select and ADV OPTIONS all match the
250
250
  CLI's `U`/`O`/`P`/`X` pickers exactly. FILE OPERATIONS (incl. ADD HOSTS FILE FROM CLIENT/
@@ -273,6 +273,13 @@ Single self-contained page, no external resources.
273
273
  `+`/`−` font size (works while typing). No keyboard shortcut for `X`/ADV OPTIONS or
274
274
  GENERATE REPORT - click required.
275
275
  - Host field feeds both ADD/DELETE; ENTER triggers the button used last, ESC clears.
276
+ - **ADD/DEL LIST** (button after DELETE; also Ctrl/Cmd+V on the page while no field has focus): paste a
277
+ host list - one per line or separated by comma/semicolon/blank, `#` comment, CIDR expanded. A live
278
+ preview shows found / new / already in list / ignored words / masks out of range; ADD and DELETE are
279
+ enabled only if they would change something (DELETE over 20 hosts asks to confirm; exact name match
280
+ only). A pasted inventory `.csv` (with the `IP Address`/`Hostname` header) also updates hosts already in
281
+ the list: IP entries (e.g. from a `.txt`) are renamed to the CSV hostname, history kept, and the CSV
282
+ values are attached. Works with `-wvc` too.
276
283
  - Files can also be dropped anywhere on the page (max 16MB, see *Host file format*).
277
284
  - Commands acknowledge immediately; a click aborts the running round.
278
285
  - **Host popup**: hovering a host row (after ~350 ms) shows everything known about it:
File without changes
File without changes
File without changes
File without changes
File without changes