linuxfabrik-lib 4.4.0__tar.gz → 5.1.0__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.
Files changed (51) hide show
  1. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/PKG-INFO +1 -1
  2. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/base.py +8 -1
  3. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/db_mysql.py +8 -6
  4. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/db_sqlite.py +38 -8
  5. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/disk.py +9 -2
  6. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/distro.py +16 -11
  7. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/dmidecode.py +2 -2
  8. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/endoflifedate.py +139 -46
  9. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/lftest.py +38 -23
  10. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/linuxfabrik_lib.egg-info/PKG-INFO +1 -1
  11. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/nextcloud.py +7 -5
  12. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/pyproject.toml +1 -1
  13. linuxfabrik_lib-5.1.0/shell.py +204 -0
  14. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/ssh.py +74 -62
  15. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/time.py +13 -3
  16. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/txt.py +3 -2
  17. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/url.py +3 -4
  18. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/version.py +18 -11
  19. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/winrm.py +0 -0
  20. linuxfabrik_lib-4.4.0/shell.py +0 -235
  21. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/LICENSE +0 -0
  22. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/README.md +0 -0
  23. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/__init__.py +0 -0
  24. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/args.py +0 -0
  25. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/cache.py +0 -0
  26. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/feedparser.py +0 -0
  27. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/globals.py +0 -0
  28. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/grassfish.py +0 -0
  29. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/huawei.py +0 -0
  30. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/human.py +0 -0
  31. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/icinga.py +0 -0
  32. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/infomaniak.py +0 -0
  33. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/jitsi.py +0 -0
  34. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/keycloak.py +0 -0
  35. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/librenms.py +0 -0
  36. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/linuxfabrik_lib.egg-info/SOURCES.txt +0 -0
  37. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/linuxfabrik_lib.egg-info/dependency_links.txt +0 -0
  38. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/linuxfabrik_lib.egg-info/requires.txt +0 -0
  39. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/linuxfabrik_lib.egg-info/top_level.txt +0 -0
  40. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/net.py +0 -0
  41. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/nodebb.py +0 -0
  42. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/powershell.py +0 -0
  43. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/psutil.py +0 -0
  44. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/qts.py +0 -0
  45. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/redfish.py +0 -0
  46. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/rocket.py +0 -0
  47. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/setup.cfg +0 -0
  48. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/smb.py +0 -0
  49. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/uptimerobot.py +0 -0
  50. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/veeam.py +0 -0
  51. {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.1.0}/wildfly.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linuxfabrik-lib
3
- Version: 4.4.0
3
+ Version: 5.1.0
4
4
  Summary: Python libraries used in various Linuxfabrik projects, including the 'Linuxfabrik Monitoring Plugins' project.
5
5
  Author-email: "Linuxfabrik GmbH, Zurich, Switzerland" <info@linuxfabrik.ch>
6
6
  License: This is free and unencumbered software released into the public domain.
@@ -11,7 +11,7 @@
11
11
  """Provides very common every-day functions."""
12
12
 
13
13
  __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
14
- __version__ = '2026051002'
14
+ __version__ = '2026061201'
15
15
 
16
16
  import html
17
17
  import numbers
@@ -119,6 +119,9 @@ def cu(msg=None):
119
119
  tb = format_exc() if has_traceback else None
120
120
 
121
121
  if msg is not None:
122
+ # Normalize line endings to LF (see oao); error output may also carry
123
+ # CRLF, for example when a Windows command prints its error to stdout.
124
+ msg = msg.replace('\r\n', '\n').replace('\r', '\n')
122
125
  msg = (
123
126
  txt.sanitize_sensitive_data(msg).strip().replace('<', "'").replace('>', "'")
124
127
  )
@@ -628,6 +631,10 @@ def oao(msg, state=STATE_OK, perfdata='', always_ok=False):
628
631
  (and exits with code 2)
629
632
 
630
633
  """
634
+ # Normalize line endings to LF. Output captured on Windows (or read from a
635
+ # file or HTTP response) can carry CRLF or stray CR, which a web UI showing
636
+ # the output with `white-space: pre-wrap` would render as an extra line break.
637
+ msg = msg.replace('\r\n', '\n').replace('\r', '\n')
631
638
  msg = html.escape(
632
639
  txt.sanitize_sensitive_data(msg.strip()),
633
640
  quote=False,
@@ -20,7 +20,7 @@ from .globals import STATE_UNKNOWN
20
20
  warnings.filterwarnings('ignore', category=UserWarning, module='pymysql')
21
21
 
22
22
  __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
23
- __version__ = '2026060201'
23
+ __version__ = '2026061201'
24
24
 
25
25
  try:
26
26
  import pymysql.cursors
@@ -302,7 +302,9 @@ def _align_session_collation(conn):
302
302
  # Whitelist identifiers to keep the formatted `SET NAMES` safe from
303
303
  # any caller-controlled value (the row comes from a system view, but
304
304
  # defence in depth doesn't cost anything).
305
- if not (re.match(r'^[A-Za-z0-9_]+$', cs) and re.match(r'^[A-Za-z0-9_]+$', coll)):
305
+ if not (
306
+ re.match(r'^[A-Za-z0-9_]+$', cs) and re.match(r'^[A-Za-z0-9_]+$', coll)
307
+ ):
306
308
  return
307
309
  with conn.cursor() as cursor:
308
310
  cursor.execute(f'set names {cs} collate {coll}')
@@ -541,10 +543,10 @@ def get_server_info(banner=None):
541
543
  from . import shell
542
544
 
543
545
  for command in (
544
- 'mysqld --version',
545
- 'mariadbd --version',
546
- 'mariadb --version',
547
- 'mysql --version',
546
+ ['mysqld', '--version'],
547
+ ['mariadbd', '--version'],
548
+ ['mariadb', '--version'],
549
+ ['mysql', '--version'],
548
550
  ):
549
551
  success, result = shell.shell_exec(command)
550
552
  if not success:
@@ -28,7 +28,7 @@ This is one typical use case of this library (taken from `disk-io`):
28
28
  """
29
29
 
30
30
  __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
31
- __version__ = '2026060201'
31
+ __version__ = '2026061701'
32
32
 
33
33
  import csv
34
34
  import hashlib
@@ -355,7 +355,8 @@ def create_index(
355
355
  If `True`, creates a unique index.
356
356
  If `False`, creates a standard (non-unique) index. Defaults to `False`.
357
357
  - **delete_db_on_operational_error** (`bool`, optional):
358
- If `True`, deletes the database file when an `OperationalError` occurs.
358
+ If `True`, deletes the database file when the on-disk database turns out
359
+ to be unusable (e.g. a schema mismatch between releases).
359
360
  Defaults to `True`.
360
361
 
361
362
  ### Returns
@@ -467,7 +468,8 @@ def cut(conn, table='perfdata', _max=5, delete_db_on_operational_error=True):
467
468
  - **_max** (`int`, optional):
468
469
  Number of most recent records to keep. Defaults to `5`.
469
470
  - **delete_db_on_operational_error** (`bool`, optional):
470
- If `True`, deletes the database file when an `OperationalError` occurs.
471
+ If `True`, deletes the database file when the on-disk database turns out
472
+ to be unusable (e.g. a schema mismatch between releases).
471
473
  Defaults to `True`.
472
474
 
473
475
  ### Returns
@@ -530,7 +532,8 @@ def delete(conn, sql, data=None, delete_db_on_operational_error=True):
530
532
  Dictionary of parameters to bind to the SQL statement.
531
533
  Defaults to an empty dict (no parameters).
532
534
  - **delete_db_on_operational_error** (`bool`, optional):
533
- If `True`, deletes the database file when an `OperationalError` occurs.
535
+ If `True`, deletes the database file when the on-disk database turns out
536
+ to be unusable (e.g. a schema mismatch between releases).
534
537
  Defaults to `True`.
535
538
 
536
539
  ### Returns
@@ -563,6 +566,14 @@ def delete(conn, sql, data=None, delete_db_on_operational_error=True):
563
566
  if delete_db_on_operational_error:
564
567
  rm_db(conn)
565
568
  return False, f'Operational Error: {e}, Query: {sql}, Data: {data}'
569
+ except (sqlite3.IntegrityError, sqlite3.DataError) as e:
570
+ # A constraint or data error (e.g. a NOT NULL column that a newer or
571
+ # older release no longer writes) means the on-disk schema no
572
+ # longer matches the data being written. Deleting the database lets the
573
+ # next run rebuild a valid cache from scratch.
574
+ if delete_db_on_operational_error:
575
+ rm_db(conn)
576
+ return False, f'Integrity Error: {e}, Query: {sql}, Data: {data}'
566
577
  except Exception as e:
567
578
  return False, f'Query failed: {sql}, Error: {e}, Data: {data}'
568
579
 
@@ -906,7 +917,8 @@ def insert(conn, data, table='perfdata', delete_db_on_operational_error=True):
906
917
  Name of the table to insert into.
907
918
  Defaults to `'perfdata'`.
908
919
  - **delete_db_on_operational_error** (`bool`, optional):
909
- If `True`, deletes the database file when an `OperationalError` occurs.
920
+ If `True`, deletes the database file when the on-disk database turns out
921
+ to be unusable (e.g. a schema mismatch between releases).
910
922
  Defaults to `True`.
911
923
 
912
924
  ### Returns
@@ -946,6 +958,14 @@ def insert(conn, data, table='perfdata', delete_db_on_operational_error=True):
946
958
  if delete_db_on_operational_error:
947
959
  rm_db(conn)
948
960
  return False, f'Operational Error: {e}, Query: {sql}, Data: {data}'
961
+ except (sqlite3.IntegrityError, sqlite3.DataError) as e:
962
+ # A constraint or data error (e.g. a NOT NULL column that a newer or
963
+ # older release no longer writes) means the on-disk schema no
964
+ # longer matches the data being written. Deleting the database lets the
965
+ # next run rebuild a valid cache from scratch.
966
+ if delete_db_on_operational_error:
967
+ rm_db(conn)
968
+ return False, f'Integrity Error: {e}, Query: {sql}, Data: {data}'
949
969
  except Exception as e:
950
970
  return False, f'Query failed: {sql}, Error: {e}, Data: {data}'
951
971
 
@@ -1030,7 +1050,7 @@ def per_second_deltas(filename, name, counters):
1030
1050
  # subsequent drop_table() with "Cannot operate on a closed database".
1031
1051
  ok, _ = insert(conn, row, delete_db_on_operational_error=False)
1032
1052
  if not ok:
1033
- # Schema mismatch from a previous plugin version (different counter
1053
+ # Schema mismatch from a previous release (different counter
1034
1054
  # columns or NOT NULL constraints). Rebuild the table from the
1035
1055
  # current schema; we lose the previous baseline but auto-recover on
1036
1056
  # the next run.
@@ -1130,7 +1150,8 @@ def replace(conn, data, table='perfdata', delete_db_on_operational_error=True):
1130
1150
  Name of the table to operate on.
1131
1151
  Defaults to `'perfdata'`.
1132
1152
  - **delete_db_on_operational_error** (`bool`, optional):
1133
- If `True`, deletes the database file when an `OperationalError` occurs.
1153
+ If `True`, deletes the database file when the on-disk database turns out
1154
+ to be unusable (e.g. a schema mismatch between releases).
1134
1155
  Defaults to `True`.
1135
1156
 
1136
1157
  ### Returns
@@ -1169,6 +1190,14 @@ def replace(conn, data, table='perfdata', delete_db_on_operational_error=True):
1169
1190
  if delete_db_on_operational_error:
1170
1191
  rm_db(conn)
1171
1192
  return False, f'Operational Error: {e}, Query: {sql}, Data: {data}'
1193
+ except (sqlite3.IntegrityError, sqlite3.DataError) as e:
1194
+ # A constraint or data error (e.g. a NOT NULL column that a newer or
1195
+ # older release no longer writes) means the on-disk schema no
1196
+ # longer matches the data being written. Deleting the database lets the
1197
+ # next run rebuild a valid cache from scratch.
1198
+ if delete_db_on_operational_error:
1199
+ rm_db(conn)
1200
+ return False, f'Integrity Error: {e}, Query: {sql}, Data: {data}'
1172
1201
  except Exception as e:
1173
1202
  return False, f'Query failed: {sql}, Error: {e}, Data: {data}'
1174
1203
 
@@ -1237,7 +1266,8 @@ def select(
1237
1266
  If `True`, return results as a list of dictionaries.
1238
1267
  If `False`, return raw SQLite row objects. Defaults to `True`.
1239
1268
  - **delete_db_on_operational_error** (`bool`, optional):
1240
- If `True`, deletes the database file when an `OperationalError` occurs.
1269
+ If `True`, deletes the database file when the on-disk database turns out
1270
+ to be unusable (e.g. a schema mismatch between releases).
1241
1271
  Defaults to `True`.
1242
1272
 
1243
1273
  ### Returns
@@ -13,7 +13,7 @@ partitions, grepping a file, etc.
13
13
  """
14
14
 
15
15
  __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
16
- __version__ = '2026060801'
16
+ __version__ = '2026061201'
17
17
 
18
18
  import csv
19
19
  import os
@@ -747,7 +747,14 @@ def udevadm(device, _property):
747
747
  >>> udevadm('/dev/linuxfabrik', 'DEVNAME')
748
748
  ''
749
749
  """
750
- success, result = shell.shell_exec(f'udevadm info --query=property --name={device}')
750
+ # Only query real device nodes under /dev. Resolving the path first defends
751
+ # against a crafted `device` that points elsewhere or smuggles arguments.
752
+ device = os.path.realpath(device)
753
+ if not device.startswith('/dev/'):
754
+ return ''
755
+ success, result = shell.shell_exec(
756
+ ['udevadm', 'info', '--query=property', f'--name={device}']
757
+ )
751
758
  if not success:
752
759
  return ''
753
760
  stdout, _, _ = result
@@ -17,15 +17,13 @@ Source Code is taken, converted, shortened and modified from:
17
17
  """
18
18
 
19
19
  __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
20
- __version__ = '2025061001'
20
+ __version__ = '2026061201'
21
21
 
22
22
 
23
23
  import os
24
24
  import platform
25
25
  import re
26
26
 
27
- from . import shell
28
-
29
27
  # --- Static mappings ---
30
28
 
31
29
  OSDIST_LIST = (
@@ -430,15 +428,22 @@ def get_distribution_facts():
430
428
  distro = facts.get('distribution_file_variety', 'distribution')
431
429
  facts['os_family'] = _map_os_family(distro) # returns 'RedHat', for example
432
430
 
433
- # hardcoded command, no user input; shell=True needed to source the file
434
- cmd = '. /etc/os-release && echo "$NAME $VERSION"'
435
- success, result = shell.shell_exec(cmd, shell=True) # nosec B604
436
- if not success:
431
+ # read NAME and VERSION from /etc/os-release
432
+ values = {}
433
+ try:
434
+ with open('/etc/os-release', encoding='utf-8') as os_release:
435
+ for line in os_release:
436
+ line = line.strip()
437
+ if not line or line.startswith('#') or '=' not in line:
438
+ continue
439
+ key, value = line.split('=', 1)
440
+ values[key] = value.strip().strip('"').strip("'")
441
+ except OSError:
437
442
  return facts
438
443
 
439
- stdout, _, _ = result
440
- facts['os_info'] = (
441
- stdout.strip()
442
- ) # returns 'Fedora Linux 41 (Workstation Edition)', for example
444
+ # returns 'Fedora Linux 41 (Workstation Edition)', for example
445
+ facts['os_info'] = ' '.join(
446
+ part for part in (values.get('NAME'), values.get('VERSION')) if part
447
+ )
443
448
 
444
449
  return facts
@@ -14,7 +14,7 @@ Copied and refactored from py-dmidecode (https://github.com/zaibon/py-dmidecode)
14
14
  """
15
15
 
16
16
  __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
17
- __version__ = '2026060201'
17
+ __version__ = '2026061201'
18
18
 
19
19
  import re
20
20
 
@@ -511,7 +511,7 @@ def get_data():
511
511
  ...
512
512
  }
513
513
  """
514
- success, result = shell.shell_exec('sudo dmidecode')
514
+ success, result = shell.shell_exec(['sudo', 'dmidecode'])
515
515
  if not success:
516
516
  return False
517
517
 
@@ -15,15 +15,15 @@ Internet."""
15
15
  # pylint: disable=C0302
16
16
 
17
17
  __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
18
- __version__ = '2026060201' # adapt this if something in this script changed fundamentally
18
+ __version__ = '2026061001' # adapt this if something in this script changed fundamentally
19
19
 
20
20
  ENDOFLIFE_DATE = {
21
21
 
22
- 'https://endoflife.date/api/apache.json': [
22
+ 'https://endoflife.date/api/apache-http-server.json': [
23
23
  { 'cycle': '2.4',
24
24
  'eol': False,
25
- 'latest': '2.4.67',
26
- 'latestReleaseDate': '2026-05-04',
25
+ 'latest': '2.4.68',
26
+ 'latestReleaseDate': '2026-06-08',
27
27
  'link': 'https://downloads.apache.org/httpd/CHANGES_2.4',
28
28
  'lts': False,
29
29
  'releaseDate': '2012-02-21'},
@@ -501,24 +501,31 @@ ENDOFLIFE_DATE = {
501
501
 
502
502
 
503
503
  'https://endoflife.date/api/gitlab.json': [
504
+ { 'cycle': '19.1',
505
+ 'eol': '2026-09-17',
506
+ 'latest': '19.1.0',
507
+ 'latestReleaseDate': '2026-06-17',
508
+ 'lts': False,
509
+ 'releaseDate': '2026-06-18',
510
+ 'support': '2026-07-16'},
504
511
  { 'cycle': '19.0',
505
512
  'eol': '2026-08-20',
506
- 'latest': '19.0.1',
507
- 'latestReleaseDate': '2026-05-26',
513
+ 'latest': '19.0.2',
514
+ 'latestReleaseDate': '2026-06-10',
508
515
  'lts': False,
509
516
  'releaseDate': '2026-05-21',
510
517
  'support': '2026-06-18'},
511
518
  { 'cycle': '18.11',
512
519
  'eol': '2026-07-16',
513
- 'latest': '18.11.4',
514
- 'latestReleaseDate': '2026-05-26',
520
+ 'latest': '18.11.5',
521
+ 'latestReleaseDate': '2026-06-10',
515
522
  'lts': False,
516
523
  'releaseDate': '2026-04-16',
517
524
  'support': '2026-05-21'},
518
525
  { 'cycle': '18.10',
519
526
  'eol': '2026-06-18',
520
- 'latest': '18.10.7',
521
- 'latestReleaseDate': '2026-05-26',
527
+ 'latest': '18.10.8',
528
+ 'latestReleaseDate': '2026-06-10',
522
529
  'lts': False,
523
530
  'releaseDate': '2026-03-19',
524
531
  'support': '2026-04-16'},
@@ -956,7 +963,7 @@ ENDOFLIFE_DATE = {
956
963
  { 'cycle': '12.4',
957
964
  'eol': '2027-05-24',
958
965
  'latest': '12.4.4',
959
- 'latestReleaseDate': '2026-06-04',
966
+ 'latestReleaseDate': '2026-06-09',
960
967
  'lts': False,
961
968
  'releaseDate': '2026-02-24',
962
969
  'support': '2026-04-17'},
@@ -1140,7 +1147,7 @@ ENDOFLIFE_DATE = {
1140
1147
 
1141
1148
  'https://endoflife.date/api/graylog.json': [
1142
1149
  { 'cycle': '7.1',
1143
- 'eol': False,
1150
+ 'eol': '2027-05-04',
1144
1151
  'latest': '7.1.3',
1145
1152
  'latestReleaseDate': '2026-06-03',
1146
1153
  'lts': False,
@@ -1837,6 +1844,12 @@ ENDOFLIFE_DATE = {
1837
1844
 
1838
1845
 
1839
1846
  'https://endoflife.date/api/mastodon.json': [
1847
+ { 'cycle': '4.6',
1848
+ 'eol': False,
1849
+ 'latest': '4.6.0',
1850
+ 'latestReleaseDate': '2026-06-17',
1851
+ 'lts': False,
1852
+ 'releaseDate': '2026-06-17'},
1840
1853
  { 'cycle': '4.5',
1841
1854
  'eol': False,
1842
1855
  'latest': '4.5.11',
@@ -1844,7 +1857,7 @@ ENDOFLIFE_DATE = {
1844
1857
  'lts': False,
1845
1858
  'releaseDate': '2025-11-06'},
1846
1859
  { 'cycle': '4.4',
1847
- 'eol': False,
1860
+ 'eol': '2026-12-17',
1848
1861
  'latest': '4.4.18',
1849
1862
  'latestReleaseDate': '2026-06-03',
1850
1863
  'lts': False,
@@ -1927,8 +1940,8 @@ ENDOFLIFE_DATE = {
1927
1940
  'https://endoflife.date/api/matomo.json': [
1928
1941
  { 'cycle': '5',
1929
1942
  'eol': False,
1930
- 'latest': '5.11.0',
1931
- 'latestReleaseDate': '2026-06-09',
1943
+ 'latest': '5.11.2',
1944
+ 'latestReleaseDate': '2026-06-17',
1932
1945
  'lts': False,
1933
1946
  'releaseDate': '2023-12-18',
1934
1947
  'support': True},
@@ -2174,8 +2187,8 @@ ENDOFLIFE_DATE = {
2174
2187
  'https://endoflife.date/api/mysql.json': [
2175
2188
  { 'cycle': '9.7',
2176
2189
  'eol': '2034-04-21',
2177
- 'latest': '9.7.0',
2178
- 'latestReleaseDate': '2026-04-07',
2190
+ 'latest': '9.7.1',
2191
+ 'latestReleaseDate': '2026-06-03',
2179
2192
  'link': 'https://dev.mysql.com/doc/relnotes/mysql/9.7/en/news-9-7-0.html',
2180
2193
  'lts': True,
2181
2194
  'releaseDate': '2026-04-21',
@@ -2237,8 +2250,8 @@ ENDOFLIFE_DATE = {
2237
2250
  'support': '2024-10-15'},
2238
2251
  { 'cycle': '8.4',
2239
2252
  'eol': '2032-04-30',
2240
- 'latest': '8.4.9',
2241
- 'latestReleaseDate': '2026-04-07',
2253
+ 'latest': '8.4.10',
2254
+ 'latestReleaseDate': '2026-06-03',
2242
2255
  'lts': True,
2243
2256
  'releaseDate': '2024-04-10',
2244
2257
  'support': '2029-04-30'},
@@ -2297,6 +2310,12 @@ ENDOFLIFE_DATE = {
2297
2310
 
2298
2311
 
2299
2312
  'https://endoflife.date/api/nextcloud.json': [
2313
+ { 'cycle': '34',
2314
+ 'eol': '2027-06-30',
2315
+ 'latest': '34.0.0',
2316
+ 'latestReleaseDate': '2026-06-08',
2317
+ 'lts': False,
2318
+ 'releaseDate': '2026-06-09'},
2300
2319
  { 'cycle': '33',
2301
2320
  'eol': '2027-02-28',
2302
2321
  'latest': '33.0.5',
@@ -2636,29 +2655,29 @@ ENDOFLIFE_DATE = {
2636
2655
  'https://endoflife.date/api/postfix.json': [
2637
2656
  { 'cycle': '3.11',
2638
2657
  'eol': False,
2639
- 'latest': '3.11.3',
2640
- 'latestReleaseDate': '2026-05-18',
2658
+ 'latest': '3.11.4',
2659
+ 'latestReleaseDate': '2026-06-18',
2641
2660
  'link': 'https://www.postfix.org/announcements/postfix-3.11.0.html',
2642
2661
  'lts': False,
2643
2662
  'releaseDate': '2026-03-06'},
2644
2663
  { 'cycle': '3.10',
2645
2664
  'eol': False,
2646
- 'latest': '3.10.10',
2647
- 'latestReleaseDate': '2026-05-18',
2665
+ 'latest': '3.10.11',
2666
+ 'latestReleaseDate': '2026-06-18',
2648
2667
  'link': 'https://www.postfix.org/announcements/postfix-3.10.0.html',
2649
2668
  'lts': False,
2650
2669
  'releaseDate': '2025-02-16'},
2651
2670
  { 'cycle': '3.9',
2652
2671
  'eol': False,
2653
- 'latest': '3.9.11',
2654
- 'latestReleaseDate': '2026-05-18',
2672
+ 'latest': '3.9.12',
2673
+ 'latestReleaseDate': '2026-06-18',
2655
2674
  'link': 'https://www.postfix.org/announcements/postfix-3.9.2.html',
2656
2675
  'lts': False,
2657
2676
  'releaseDate': '2024-03-06'},
2658
2677
  { 'cycle': '3.8',
2659
2678
  'eol': False,
2660
- 'latest': '3.8.17',
2661
- 'latestReleaseDate': '2026-05-18',
2679
+ 'latest': '3.8.18',
2680
+ 'latestReleaseDate': '2026-06-18',
2662
2681
  'link': 'https://www.postfix.org/announcements/postfix-3.9.2.html',
2663
2682
  'lts': False,
2664
2683
  'releaseDate': '2023-04-17'},
@@ -2944,16 +2963,16 @@ ENDOFLIFE_DATE = {
2944
2963
  'https://endoflife.date/api/python.json': [
2945
2964
  { 'cycle': '3.14',
2946
2965
  'eol': '2030-10-31',
2947
- 'latest': '3.14.5',
2948
- 'latestReleaseDate': '2026-05-10',
2966
+ 'latest': '3.14.6',
2967
+ 'latestReleaseDate': '2026-06-10',
2949
2968
  'lts': False,
2950
2969
  'pep': 'PEP-0745',
2951
2970
  'releaseDate': '2025-10-07',
2952
2971
  'support': '2027-10-01'},
2953
2972
  { 'cycle': '3.13',
2954
2973
  'eol': '2029-10-31',
2955
- 'latest': '3.13.13',
2956
- 'latestReleaseDate': '2026-04-07',
2974
+ 'latest': '3.13.14',
2975
+ 'latestReleaseDate': '2026-06-10',
2957
2976
  'lts': False,
2958
2977
  'pep': 'PEP-0719',
2959
2978
  'releaseDate': '2024-10-07',
@@ -3273,46 +3292,53 @@ ENDOFLIFE_DATE = {
3273
3292
  ],
3274
3293
 
3275
3294
 
3276
- 'https://endoflife.date/api/rocketchat.json': [
3295
+ 'https://endoflife.date/api/rocket-chat.json': [
3296
+ { 'cycle': '8.5',
3297
+ 'eol': '2027-06-10',
3298
+ 'latest': '8.5.1',
3299
+ 'latestReleaseDate': '2026-06-15',
3300
+ 'lts': True,
3301
+ 'releaseDate': '2026-06-10',
3302
+ 'support': '2026-06-10'},
3277
3303
  { 'cycle': '8.4',
3278
3304
  'eol': '2026-10-31',
3279
- 'latest': '8.4.3',
3280
- 'latestReleaseDate': '2026-06-02',
3305
+ 'latest': '8.4.4',
3306
+ 'latestReleaseDate': '2026-06-15',
3281
3307
  'lts': False,
3282
3308
  'releaseDate': '2026-04-30',
3283
3309
  'support': '2026-04-30'},
3284
3310
  { 'cycle': '8.3',
3285
3311
  'eol': '2026-10-31',
3286
- 'latest': '8.3.5',
3287
- 'latestReleaseDate': '2026-06-02',
3312
+ 'latest': '8.3.6',
3313
+ 'latestReleaseDate': '2026-06-15',
3288
3314
  'lts': False,
3289
3315
  'releaseDate': '2026-04-07',
3290
3316
  'support': '2026-04-07'},
3291
3317
  { 'cycle': '8.2',
3292
3318
  'eol': '2026-09-30',
3293
- 'latest': '8.2.5',
3294
- 'latestReleaseDate': '2026-06-02',
3319
+ 'latest': '8.2.6',
3320
+ 'latestReleaseDate': '2026-06-15',
3295
3321
  'lts': False,
3296
3322
  'releaseDate': '2026-03-02',
3297
3323
  'support': '2026-03-02'},
3298
3324
  { 'cycle': '8.1',
3299
3325
  'eol': '2026-08-31',
3300
- 'latest': '8.1.5',
3301
- 'latestReleaseDate': '2026-05-22',
3326
+ 'latest': '8.1.6',
3327
+ 'latestReleaseDate': '2026-06-15',
3302
3328
  'lts': False,
3303
3329
  'releaseDate': '2026-02-10',
3304
3330
  'support': '2026-02-10'},
3305
3331
  { 'cycle': '8.0',
3306
3332
  'eol': '2026-06-30',
3307
- 'latest': '8.0.6',
3308
- 'latestReleaseDate': '2026-05-22',
3333
+ 'latest': '8.0.7',
3334
+ 'latestReleaseDate': '2026-06-15',
3309
3335
  'lts': False,
3310
3336
  'releaseDate': '2026-01-12',
3311
3337
  'support': '2026-01-12'},
3312
3338
  { 'cycle': '7.13',
3313
3339
  'eol': '2026-05-31',
3314
- 'latest': '7.13.8',
3315
- 'latestReleaseDate': '2026-05-22',
3340
+ 'latest': '7.13.9',
3341
+ 'latestReleaseDate': '2026-06-15',
3316
3342
  'lts': False,
3317
3343
  'releaseDate': '2025-12-05',
3318
3344
  'support': '2025-12-05'},
@@ -3332,8 +3358,8 @@ ENDOFLIFE_DATE = {
3332
3358
  'support': '2025-10-17'},
3333
3359
  { 'cycle': '7.10',
3334
3360
  'eol': '2026-06-30',
3335
- 'latest': '7.10.12',
3336
- 'latestReleaseDate': '2026-05-25',
3361
+ 'latest': '7.10.13',
3362
+ 'latestReleaseDate': '2026-06-15',
3337
3363
  'lts': True,
3338
3364
  'releaseDate': '2025-09-04',
3339
3365
  'support': '2025-09-04'},
@@ -3862,6 +3888,73 @@ ENDOFLIFE_DATE = {
3862
3888
  ],
3863
3889
 
3864
3890
 
3891
+ 'https://endoflife.date/api/tomcat.json': [
3892
+ { 'cycle': '11.0',
3893
+ 'eol': False,
3894
+ 'latest': '11.0.23',
3895
+ 'latestReleaseDate': '2026-06-17',
3896
+ 'lts': False,
3897
+ 'minJavaVersion': '17',
3898
+ 'releaseDate': '2024-10-03'},
3899
+ { 'cycle': '10.1',
3900
+ 'eol': False,
3901
+ 'latest': '10.1.56',
3902
+ 'latestReleaseDate': '2026-06-17',
3903
+ 'lts': False,
3904
+ 'minJavaVersion': '11',
3905
+ 'releaseDate': '2022-09-23'},
3906
+ { 'cycle': '10.0',
3907
+ 'eol': '2022-10-31',
3908
+ 'latest': '10.0.27',
3909
+ 'latestReleaseDate': '2022-10-03',
3910
+ 'lts': False,
3911
+ 'minJavaVersion': '8',
3912
+ 'releaseDate': '2020-12-03'},
3913
+ { 'cycle': '9.0',
3914
+ 'eol': '2027-03-31',
3915
+ 'latest': '9.0.119',
3916
+ 'latestReleaseDate': '2026-06-18',
3917
+ 'lts': False,
3918
+ 'minJavaVersion': '8',
3919
+ 'releaseDate': '2017-09-27'},
3920
+ { 'cycle': '8.5',
3921
+ 'eol': '2024-03-31',
3922
+ 'latest': '8.5.100',
3923
+ 'latestReleaseDate': '2024-03-19',
3924
+ 'lts': False,
3925
+ 'minJavaVersion': '7',
3926
+ 'releaseDate': '2016-03-17'},
3927
+ { 'cycle': '8.0',
3928
+ 'eol': '2018-06-30',
3929
+ 'latest': '8.0.53',
3930
+ 'latestReleaseDate': '2018-06-29',
3931
+ 'lts': False,
3932
+ 'minJavaVersion': '7',
3933
+ 'releaseDate': '2014-01-29'},
3934
+ { 'cycle': '7',
3935
+ 'eol': '2021-03-31',
3936
+ 'latest': '7.0.109',
3937
+ 'latestReleaseDate': '2021-04-22',
3938
+ 'lts': False,
3939
+ 'minJavaVersion': '6',
3940
+ 'releaseDate': '2013-01-10'},
3941
+ { 'cycle': '6',
3942
+ 'eol': '2016-12-31',
3943
+ 'latest': '6.0.53',
3944
+ 'latestReleaseDate': '2017-04-02',
3945
+ 'lts': False,
3946
+ 'minJavaVersion': '5',
3947
+ 'releaseDate': '2006-10-21'},
3948
+ { 'cycle': '5',
3949
+ 'eol': '2012-09-30',
3950
+ 'latest': '5.5.36',
3951
+ 'latestReleaseDate': '2012-10-10',
3952
+ 'lts': False,
3953
+ 'minJavaVersion': '1.4',
3954
+ 'releaseDate': '2003-09-06'},
3955
+ ],
3956
+
3957
+
3865
3958
  'https://endoflife.date/api/valkey.json': [
3866
3959
  { 'cycle': '9.1',
3867
3960
  'eol': False,