feat: script client inventaire.py (stdlib only, 28 tests)

CLI argparse : --host, --port, --dry-run/-n, --debug, --output
Variables env : MES_HDD_HOST, MES_HDD_PORT
Détection OS : proxmox/ubuntu/debian
SMART en français : ok/warn/fail/unavailable avec détail lisible
Partitions : fstype, UUID, espace, LVM, /home users
Distribution : curl | sudo python3 -

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gilles Soulier
2026-05-28 20:15:46 +02:00
parent d269ca8bc9
commit c0e737244c
2 changed files with 775 additions and 0 deletions
+234
View File
@@ -0,0 +1,234 @@
import sys, os, json
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from unittest.mock import patch, MagicMock, mock_open, call
import inventaire
# ── parse_args ────────────────────────────────────────────────────────────────
def test_args_défauts(monkeypatch):
monkeypatch.delenv("MES_HDD_HOST", raising=False)
monkeypatch.delenv("MES_HDD_PORT", raising=False)
with patch("sys.argv", ["inventaire.py"]):
args = inventaire.parse_args()
assert args.host == "10.0.0.50"
assert args.port == 8088
assert args.dry_run is False
assert args.debug is False
assert args.output is None
def test_args_host_port():
with patch("sys.argv", ["inventaire.py", "--host", "192.168.1.10", "--port", "9000"]):
args = inventaire.parse_args()
assert args.host == "192.168.1.10"
assert args.port == 9000
def test_args_dry_run():
with patch("sys.argv", ["inventaire.py", "-n"]):
args = inventaire.parse_args()
assert args.dry_run is True
def test_args_debug():
with patch("sys.argv", ["inventaire.py", "--debug"]):
args = inventaire.parse_args()
assert args.debug is True
def test_args_output():
with patch("sys.argv", ["inventaire.py", "--output", "/tmp/inv.json"]):
args = inventaire.parse_args()
assert args.output == "/tmp/inv.json"
def test_args_depuis_env(monkeypatch):
monkeypatch.setenv("MES_HDD_HOST", "10.0.0.99")
monkeypatch.setenv("MES_HDD_PORT", "9999")
with patch("sys.argv", ["inventaire.py"]):
args = inventaire.parse_args()
assert args.host == "10.0.0.99"
assert args.port == 9999
# ── detect_os ─────────────────────────────────────────────────────────────────
def test_detect_proxmox_via_variant():
content = 'ID=debian\nVARIANT_ID=proxmox\nVERSION_ID="8.2"\n'
with patch("builtins.open", mock_open(read_data=content)):
with patch("os.path.isdir", return_value=False):
os_type, version = inventaire.detect_os()
assert os_type == "proxmox"
assert version == "8.2"
def test_detect_proxmox_via_pve_dir():
content = 'ID=debian\nVERSION_ID="8.2"\n'
with patch("builtins.open", mock_open(read_data=content)):
with patch("os.path.isdir", side_effect=lambda p: "/etc/pve" in p):
os_type, _ = inventaire.detect_os()
assert os_type == "proxmox"
def test_detect_ubuntu():
content = 'ID=ubuntu\nVERSION_ID="22.04"\n'
with patch("builtins.open", mock_open(read_data=content)):
with patch("os.path.isdir", return_value=False):
os_type, version = inventaire.detect_os()
assert os_type == "ubuntu"
assert version == "22.04"
def test_detect_debian():
content = 'ID=debian\nVERSION_ID="12"\n'
with patch("builtins.open", mock_open(read_data=content)):
with patch("os.path.isdir", return_value=False):
os_type, version = inventaire.detect_os()
assert os_type == "debian"
def test_detect_os_fichier_absent():
with patch("builtins.open", side_effect=FileNotFoundError):
with patch("os.path.isdir", return_value=False):
os_type, version = inventaire.detect_os()
assert os_type == "debian"
assert version == ""
# ── bytes_human ───────────────────────────────────────────────────────────────
def test_bytes_human_to():
assert inventaire.bytes_human(1_099_511_627_776) == "1.0 To"
def test_bytes_human_go():
assert inventaire.bytes_human(1_073_741_824) == "1.0 Go"
def test_bytes_human_mo():
assert inventaire.bytes_human(500 * 1024 * 1024) == "500.0 Mo"
def test_bytes_human_none():
assert inventaire.bytes_human(None) == "?"
# ── get_smart ─────────────────────────────────────────────────────────────────
SMART_PASSED = """
SMART overall-health self-assessment test result: PASSED
190 Airflow_Temperature_Cel 0x0022 073 060 045 Old_age Always - 27
9 Power_On_Hours 0x0032 099 099 000 Old_age Always - 2847
5 Reallocated_Sector_Ct 0x0033 100 100 036 Pre-fail Always - 0
197 Current_Pending_Sector 0x0012 100 100 000 Old_age Always - 0
198 Offline_Uncorrectable 0x0010 100 100 000 Old_age Offline - 0
"""
SMART_FAILED = "SMART overall-health self-assessment test result: FAILED!"
SMART_WARN = """
SMART overall-health self-assessment test result: PASSED
5 Reallocated_Sector_Ct 0x0033 100 100 036 Pre-fail Always - 3
197 Current_Pending_Sector 0x0012 100 100 000 Old_age Always - 0
198 Offline_Uncorrectable 0x0010 100 100 000 Old_age Offline - 0
"""
def test_smart_ok():
with patch.object(inventaire, "run", return_value=SMART_PASSED):
r = inventaire.get_smart("/dev/sda")
assert r["status"] == "ok"
assert r["label"] == "Bon état"
assert r["power_on_hours"] == 2847
assert r["temperature_c"] == 27
assert r["reallocated_sectors"] == 0
assert "2847h" in r["detail"]
assert "27°C" in r["detail"]
assert "aucun secteur" in r["detail"]
def test_smart_fail():
with patch.object(inventaire, "run", return_value=SMART_FAILED):
r = inventaire.get_smart("/dev/sda")
assert r["status"] == "fail"
assert r["label"] == "Défaillance probable"
assert "remplacement" in r["detail"]
def test_smart_warn_secteurs_réalloués():
with patch.object(inventaire, "run", return_value=SMART_WARN):
r = inventaire.get_smart("/dev/sda")
assert r["status"] == "warn"
assert r["label"] == "Attention"
assert "réalloué" in r["detail"]
assert r["reallocated_sectors"] == 3
def test_smart_unavailable_commande_absente():
with patch.object(inventaire, "run", return_value=None):
r = inventaire.get_smart("/dev/sda")
assert r["status"] == "unavailable"
assert r["label"] == "SMART indisponible"
def test_smart_unavailable_résultat_inconnu():
with patch.object(inventaire, "run", return_value="Device: some output without health"):
r = inventaire.get_smart("/dev/sda")
assert r["status"] == "unavailable"
# ── get_home_users ────────────────────────────────────────────────────────────
def test_home_users_triés_par_taille():
du_output = "100000\t/home/alice\n400000\t/home/gilles\n850000000\t/home\n"
with patch.object(inventaire, "run", return_value=du_output):
with patch("os.path.isdir", return_value=True):
users = inventaire.get_home_users()
assert users[0]["user"] == "gilles"
assert users[0]["size_bytes"] == 400000
assert users[1]["user"] == "alice"
assert len(users) == 2 # /home lui-même est filtré
def test_home_users_home_absent():
with patch("os.path.isdir", return_value=False):
users = inventaire.get_home_users()
assert users == []
def test_home_users_du_échoue():
with patch("os.path.isdir", return_value=True):
with patch.object(inventaire, "run", return_value=None):
users = inventaire.get_home_users()
assert users is None
# ── post_to_api ───────────────────────────────────────────────────────────────
def test_post_to_api_succès():
payload = {"hostname": "pve1", "disks": []}
response_body = json.dumps({"accepted": 0, "hostname": "pve1"}).encode()
mock_resp = MagicMock()
mock_resp.__enter__ = MagicMock(return_value=mock_resp)
mock_resp.__exit__ = MagicMock(return_value=False)
mock_resp.read.return_value = response_body
with patch("urllib.request.urlopen", return_value=mock_resp):
inventaire.post_to_api(payload, "http://10.0.0.50:8088") # pas d'exception = succès
def test_post_to_api_http_error():
import urllib.error
payload = {"hostname": "pve1", "disks": []}
with patch("urllib.request.urlopen",
side_effect=urllib.error.HTTPError("url", 500, "Server Error", {}, None)):
with patch("sys.exit") as mock_exit:
inventaire.post_to_api(payload, "http://10.0.0.50:8088")
mock_exit.assert_called_once_with(1)
def test_post_to_api_connexion_refusée():
import urllib.error
payload = {"hostname": "pve1", "disks": []}
with patch("urllib.request.urlopen",
side_effect=urllib.error.URLError("Connection refused")):
with patch("sys.exit") as mock_exit:
inventaire.post_to_api(payload, "http://10.0.0.50:8088")
mock_exit.assert_called_once_with(1)
# ── dry_run / debug output ────────────────────────────────────────────────────
def test_dry_run_naffiche_que_json(capsys):
payload = {"hostname": "pve1", "disks": [], "os": "debian"}
inventaire.print_json(payload)
captured = capsys.readouterr()
parsed = json.loads(captured.out)
assert parsed["hostname"] == "pve1"
def test_dry_run_nenvoie_pas(capsys):
payload = {"hostname": "pve1", "disks": []}
with patch.object(inventaire, "post_to_api") as mock_post:
inventaire.print_json(payload)
mock_post.assert_not_called()