linuxfabrik-lib 4.3.0__tar.gz → 4.4.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 (50) hide show
  1. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/PKG-INFO +1 -2
  2. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/disk.py +31 -1
  3. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/endoflifedate.py +8 -2
  4. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/linuxfabrik_lib.egg-info/PKG-INFO +1 -2
  5. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/linuxfabrik_lib.egg-info/requires.txt +0 -1
  6. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/net.py +151 -43
  7. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/pyproject.toml +1 -2
  8. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/redfish.py +651 -20
  9. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/time.py +26 -7
  10. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/url.py +38 -27
  11. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/LICENSE +0 -0
  12. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/README.md +0 -0
  13. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/__init__.py +0 -0
  14. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/args.py +0 -0
  15. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/base.py +0 -0
  16. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/cache.py +0 -0
  17. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/db_mysql.py +0 -0
  18. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/db_sqlite.py +0 -0
  19. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/distro.py +0 -0
  20. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/dmidecode.py +0 -0
  21. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/feedparser.py +0 -0
  22. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/globals.py +0 -0
  23. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/grassfish.py +0 -0
  24. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/huawei.py +0 -0
  25. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/human.py +0 -0
  26. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/icinga.py +0 -0
  27. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/infomaniak.py +0 -0
  28. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/jitsi.py +0 -0
  29. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/keycloak.py +0 -0
  30. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/lftest.py +0 -0
  31. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/librenms.py +0 -0
  32. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/linuxfabrik_lib.egg-info/SOURCES.txt +0 -0
  33. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/linuxfabrik_lib.egg-info/dependency_links.txt +0 -0
  34. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/linuxfabrik_lib.egg-info/top_level.txt +0 -0
  35. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/nextcloud.py +0 -0
  36. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/nodebb.py +0 -0
  37. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/powershell.py +0 -0
  38. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/psutil.py +0 -0
  39. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/qts.py +0 -0
  40. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/rocket.py +0 -0
  41. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/setup.cfg +0 -0
  42. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/shell.py +0 -0
  43. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/smb.py +0 -0
  44. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/ssh.py +0 -0
  45. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/txt.py +0 -0
  46. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/uptimerobot.py +0 -0
  47. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/veeam.py +0 -0
  48. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/version.py +0 -0
  49. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/wildfly.py +0 -0
  50. {linuxfabrik_lib-4.3.0 → linuxfabrik_lib-4.4.0}/winrm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linuxfabrik-lib
3
- Version: 4.3.0
3
+ Version: 4.4.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.
@@ -59,7 +59,6 @@ License-File: LICENSE
59
59
  Requires-Dist: beautifulsoup4
60
60
  Requires-Dist: httpx[http2]
61
61
  Requires-Dist: lxml
62
- Requires-Dist: netifaces
63
62
  Requires-Dist: psutil
64
63
  Requires-Dist: pymysql
65
64
  Requires-Dist: pypsrp
@@ -13,7 +13,7 @@ partitions, grepping a file, etc.
13
13
  """
14
14
 
15
15
  __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
16
- __version__ = '2026060601'
16
+ __version__ = '2026060801'
17
17
 
18
18
  import csv
19
19
  import os
@@ -693,6 +693,36 @@ def rm_file(filename):
693
693
  return False, f'Unknown error deleting {filename}: {e}'
694
694
 
695
695
 
696
+ def shorten_path(path):
697
+ """
698
+ Shorten a path for display.
699
+
700
+ Abbreviate a slash-separated path by reducing every component except the last one to its
701
+ first character, the way the zsh prompt shortens a long working directory. The last
702
+ component is kept in full because it usually carries the identifying information. This is
703
+ a pure string transformation; the path is not resolved and the filesystem is not touched.
704
+
705
+ ### Parameters
706
+ - **path** (`str`): A slash-separated path, for example `/etc/pki/tls/certs/002c0b4f.0`.
707
+
708
+ ### Returns
709
+ - **str**: The abbreviated path, for example `/e/p/t/c/002c0b4f.0`. A value without a
710
+ slash (a bare basename, an empty string, a sentinel like `-`) is returned unchanged.
711
+
712
+ ### Example
713
+ >>> shorten_path('/etc/pki/tls/certs/002c0b4f.0')
714
+ '/e/p/t/c/002c0b4f.0'
715
+
716
+ >>> shorten_path('index.php')
717
+ 'index.php'
718
+ """
719
+ if not path or '/' not in path:
720
+ return path
721
+ head, _, tail = path.rpartition('/')
722
+ abbrev = '/'.join(part[:1] for part in head.split('/'))
723
+ return f'{abbrev}/{tail}'
724
+
725
+
696
726
  def udevadm(device, _property):
697
727
  """
698
728
  Run `udevadm info` and extract a specific property manually.
@@ -1927,8 +1927,8 @@ ENDOFLIFE_DATE = {
1927
1927
  'https://endoflife.date/api/matomo.json': [
1928
1928
  { 'cycle': '5',
1929
1929
  'eol': False,
1930
- 'latest': '5.10.1',
1931
- 'latestReleaseDate': '2026-05-29',
1930
+ 'latest': '5.11.0',
1931
+ 'latestReleaseDate': '2026-06-09',
1932
1932
  'lts': False,
1933
1933
  'releaseDate': '2023-12-18',
1934
1934
  'support': True},
@@ -1964,6 +1964,12 @@ ENDOFLIFE_DATE = {
1964
1964
 
1965
1965
 
1966
1966
  'https://endoflife.date/api/mediawiki.json': [
1967
+ { 'cycle': '1.46',
1968
+ 'eol': '2027-05-31',
1969
+ 'latest': '1.46.0',
1970
+ 'latestReleaseDate': '2026-05-31',
1971
+ 'lts': False,
1972
+ 'releaseDate': '2026-05-31'},
1967
1973
  { 'cycle': '1.45',
1968
1974
  'eol': '2026-12-31',
1969
1975
  'latest': '1.45.3',
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linuxfabrik-lib
3
- Version: 4.3.0
3
+ Version: 4.4.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.
@@ -59,7 +59,6 @@ License-File: LICENSE
59
59
  Requires-Dist: beautifulsoup4
60
60
  Requires-Dist: httpx[http2]
61
61
  Requires-Dist: lxml
62
- Requires-Dist: netifaces
63
62
  Requires-Dist: psutil
64
63
  Requires-Dist: pymysql
65
64
  Requires-Dist: pypsrp
@@ -1,7 +1,6 @@
1
1
  beautifulsoup4
2
2
  httpx[http2]
3
3
  lxml
4
- netifaces
5
4
  psutil
6
5
  pymysql
7
6
  pypsrp
@@ -12,7 +12,7 @@
12
12
  """Provides network related functions and variables."""
13
13
 
14
14
  __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
15
- __version__ = '2026060601'
15
+ __version__ = '2026060901'
16
16
 
17
17
  import ipaddress
18
18
  import random
@@ -21,11 +21,11 @@ import socket
21
21
  import ssl
22
22
 
23
23
  try:
24
- import netifaces
24
+ import psutil
25
25
 
26
- HAVE_NETIFACES = True
26
+ HAVE_PSUTIL = True
27
27
  except ImportError:
28
- HAVE_NETIFACES = False
28
+ HAVE_PSUTIL = False
29
29
 
30
30
  from . import (
31
31
  txt, # pylint: disable=C0413
@@ -459,12 +459,98 @@ def fetch_ssl(host, port, msg=None, timeout=3):
459
459
  return False, f'Could not open SSL socket: {e}'
460
460
 
461
461
 
462
+ def _default_route_ip(family=socket.AF_INET):
463
+ """
464
+ Return the local IP address the kernel would use for the default route, or `None`.
465
+
466
+ Opens a connected UDP socket towards an off-link address and reads back the source address the
467
+ routing table selects. UDP `connect` only sets the peer, so no packet is sent; this works on
468
+ Linux, Windows and macOS without external tools or privileges, and offline as long as a default
469
+ route exists.
470
+
471
+ ### Parameters
472
+ - **family** (`int`, optional): `socket.AF_INET` (default) or `socket.AF_INET6`.
473
+
474
+ ### Returns
475
+ - **str** or **None**: The selected source address, or `None` if no route is available.
476
+ """
477
+ probe = '192.0.2.1' if family == socket.AF_INET else '2001:db8::1'
478
+ try:
479
+ sock = socket.socket(family, socket.SOCK_DGRAM)
480
+ except OSError:
481
+ return None
482
+ try:
483
+ sock.connect((probe, 9))
484
+ return sock.getsockname()[0].split('%')[0]
485
+ except OSError:
486
+ return None
487
+ finally:
488
+ sock.close()
489
+
490
+
491
+ def _default_gateway():
492
+ """
493
+ Return the IPv4 default gateway address, or `None` if it cannot be determined.
494
+
495
+ Reads the kernel routing table from `/proc/net/route` (Linux). On platforms without that file
496
+ the gateway is not available through a dependency-free, cross-platform API, so `None` is
497
+ returned rather than guessing.
498
+
499
+ ### Returns
500
+ - **str** or **None**: The default gateway IPv4 address, or `None`.
501
+ """
502
+ try:
503
+ with open('/proc/net/route', encoding='ascii') as routes:
504
+ for line in routes.read().splitlines()[1:]:
505
+ fields = line.split()
506
+ # Destination 00000000 marks the default route; flags must have RTF_GATEWAY (0x2).
507
+ if (
508
+ len(fields) > 3
509
+ and fields[1] == '00000000'
510
+ and int(fields[3], 16) & 0x2
511
+ ):
512
+ # The gateway is a little-endian hex IPv4 in column 2.
513
+ return socket.inet_ntoa(bytes.fromhex(fields[2])[::-1])
514
+ except (OSError, ValueError):
515
+ return None
516
+ return None
517
+
518
+
519
+ def _iface_for_ip(ip):
520
+ """
521
+ Return `(interface_name, netmask)` for the interface carrying `ip`, or `(None, None)`.
522
+
523
+ Matches the address against psutil's per-interface address list, so it is platform-independent.
524
+
525
+ ### Parameters
526
+ - **ip** (`str`): The IPv4 or IPv6 address to look up.
527
+
528
+ ### Returns
529
+ - **tuple** (`str` or `None`, `str` or `None`): The interface name and its netmask.
530
+ """
531
+ try:
532
+ wanted = ipaddress.ip_address(ip)
533
+ except ValueError:
534
+ return (None, None)
535
+ for iface, addrs in psutil.net_if_addrs().items():
536
+ for addr in addrs:
537
+ if addr.family not in (socket.AF_INET, socket.AF_INET6):
538
+ continue
539
+ try:
540
+ if ipaddress.ip_address((addr.address or '').split('%')[0]) == wanted:
541
+ return (iface, addr.netmask)
542
+ except ValueError:
543
+ continue
544
+ return (None, None)
545
+
546
+
462
547
  def get_netinfo():
463
548
  """
464
549
  Retrieve local and public network information.
465
550
 
466
551
  This function retrieves the system's primary IP address, netmask, CIDR mask, default gateway,
467
- and public IP address. Uses the `netifaces` library if available.
552
+ and public IP address. Addresses come from `psutil` (platform-independent); the default gateway
553
+ is read from the Linux routing table and is `None` on other platforms.
468
554
 
469
555
  ### Parameters
470
556
  - None
@@ -475,12 +561,12 @@ def get_netinfo():
475
561
  - `address` (`str`): The local IP address.
476
562
  - `mask` (`str`): The subnet mask.
477
563
  - `mask_cidr` (`str`): The subnet mask as CIDR.
478
- - `gateway` (`str`): The default gateway address.
564
+ - `gateway` (`str` or `None`): The default gateway address.
479
565
  - `public_address` (`str`): The public IP address.
480
566
 
481
567
  ### Notes
482
568
  - If fetching any required information fails, an empty list is returned.
483
- - Requires `netifaces` and `ip_to_cidr()` helper.
569
+ - Requires `psutil` and the `ip_to_cidr()` helper.
484
570
  - The `public_address` field is always `None`; callers that want the public
485
571
  IP must call `get_public_ip()` separately with a list of lookup services.
486
572
 
@@ -489,25 +575,24 @@ def get_netinfo():
489
575
  >>> print(netinfo['address'])
490
576
  '192.168.1.10'
491
577
  """
492
- if not HAVE_NETIFACES:
578
+ if not HAVE_PSUTIL:
493
579
  return []
494
580
 
495
- try:
496
- default_gateway = netifaces.gateways()['default'][netifaces.AF_INET]
497
- iface = default_gateway[1]
498
- iface_addrs = netifaces.ifaddresses(iface)[netifaces.AF_INET][0]
499
-
500
- return {
501
- 'address': iface_addrs.get('addr'),
502
- 'mask': iface_addrs.get('netmask'),
503
- 'mask_cidr': ip_to_cidr(iface_addrs.get('netmask')),
504
- 'gateway': default_gateway[0],
505
- 'public_address': None,
506
- }
507
-
508
- except (KeyError, AttributeError, IndexError):
581
+ address = _default_route_ip(socket.AF_INET)
582
+ if not address:
583
+ return []
584
+ iface, netmask = _iface_for_ip(address)
585
+ if not iface or not netmask:
509
586
  return []
510
587
 
588
+ return {
589
+ 'address': address,
590
+ 'mask': netmask,
591
+ 'mask_cidr': ip_to_cidr(netmask),
592
+ 'gateway': _default_gateway(),
593
+ 'public_address': None,
594
+ }
595
+
511
596
 
512
597
  def get_public_ip(services, insecure=False, no_proxy=False, timeout=2):
513
598
  """
@@ -597,6 +682,30 @@ def cidr_to_hosts(cidr, max_hosts=65536):
597
682
  return (True, [str(host) for host in network.hosts()])
598
683
 
599
684
 
685
+ def _ipv6_prefixlen(netmask):
686
+ """
687
+ Return the prefix length (as a string) for an IPv6 netmask reported by psutil.
688
+
689
+ psutil reports the IPv6 netmask either as a full mask ("ffff:ffff:ffff:ffff::"), as a
690
+ "mask/prefix" string, or as `None`. All three are normalised to a prefix length, defaulting to
691
+ "128" (a single host) when it cannot be derived.
692
+
693
+ ### Parameters
694
+ - **netmask** (`str` or `None`): The IPv6 netmask as reported by psutil.
695
+
696
+ ### Returns
697
+ - **str**: The prefix length, e.g. "64".
698
+ """
699
+ if not netmask:
700
+ return '128'
701
+ if '/' in netmask:
702
+ return netmask.split('/')[-1]
703
+ try:
704
+ return str(bin(int(ipaddress.IPv6Address(netmask))).count('1'))
705
+ except ipaddress.AddressValueError:
706
+ return '128'
707
+
708
+
600
709
  def get_subnet_hosts(interface=None, max_hosts=65536):
601
710
  """
602
711
  Return the usable host IP addresses of an interface's subnet.
@@ -620,14 +729,14 @@ def get_subnet_hosts(interface=None, max_hosts=65536):
620
729
  - `False` and an error message on failure.
621
730
 
622
731
  ### Notes
623
- - Requires the `netifaces` library.
732
+ - Requires the `psutil` library.
624
733
 
625
734
  ### Example
626
735
  >>> get_subnet_hosts('eth0')
627
736
  (True, ['192.168.1.1', '192.168.1.2', ..., '192.168.1.254'])
628
737
  """
629
- if not HAVE_NETIFACES:
630
- return (False, 'Python module "netifaces" is not installed.')
738
+ if not HAVE_PSUTIL:
739
+ return (False, 'Python module "psutil" is not installed.')
631
740
 
632
741
  if interface is None:
633
742
  netinfo = get_netinfo()
@@ -637,26 +746,25 @@ def get_subnet_hosts(interface=None, max_hosts=65536):
637
746
  f'{netinfo["address"]}/{netinfo["mask_cidr"]}', max_hosts=max_hosts
638
747
  )
639
748
 
640
- try:
641
- iface_addrs = netifaces.ifaddresses(interface)
642
- except ValueError:
749
+ if_addrs = psutil.net_if_addrs()
750
+ if interface not in if_addrs:
643
751
  return (False, f'No such interface "{interface}".')
644
752
 
645
- # IPv4 first, then IPv6. netifaces stores the IPv6 netmask as
646
- # "ffff:ffff:ffff:ffff::/64" and may suffix the address with a "%scope".
647
- for family in (netifaces.AF_INET, netifaces.AF_INET6):
648
- entries = iface_addrs.get(family)
649
- if not entries:
650
- continue
651
- address = (entries[0].get('addr') or '').split('%')[0]
652
- netmask = entries[0].get('netmask') or ''
653
- if not address:
654
- continue
655
- if family == netifaces.AF_INET:
656
- cidr = ip_to_cidr(netmask)
657
- else:
658
- cidr = netmask.split('/')[-1] if '/' in netmask else '128'
659
- return cidr_to_hosts(f'{address}/{cidr}', max_hosts=max_hosts)
753
+ # IPv4 first, then IPv6. psutil may suffix the address with a "%scope" and reports the IPv6
754
+ # netmask as a full mask ("ffff:ffff:ffff:ffff::") rather than a prefix length.
755
+ for family in (socket.AF_INET, socket.AF_INET6):
756
+ for addr in if_addrs[interface]:
757
+ if addr.family != family:
758
+ continue
759
+ address = (addr.address or '').split('%')[0]
760
+ netmask = addr.netmask or ''
761
+ if not address:
762
+ continue
763
+ if family == socket.AF_INET:
764
+ cidr = ip_to_cidr(netmask)
765
+ else:
766
+ cidr = _ipv6_prefixlen(netmask)
767
+ return cidr_to_hosts(f'{address}/{cidr}', max_hosts=max_hosts)
660
768
 
661
769
  return (False, f'No IPv4 or IPv6 address found on interface "{interface}".')
662
770
 
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "linuxfabrik-lib"
9
- version = "4.3.0"
9
+ version = "4.4.0"
10
10
  description = "Python libraries used in various Linuxfabrik projects, including the 'Linuxfabrik Monitoring Plugins' project."
11
11
  readme = "README.md"
12
12
  authors = [
@@ -40,7 +40,6 @@ dependencies = [
40
40
  "beautifulsoup4",
41
41
  "httpx[http2]",
42
42
  "lxml",
43
- "netifaces",
44
43
  "psutil",
45
44
  "pymysql",
46
45
  "pypsrp",