lablink-cli 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,112 @@
1
+ """Auto-detection helpers for `lablink client register` (BYO-box facts).
2
+
3
+ Each helper has a single, narrow responsibility and returns a value or
4
+ None / a benign default — never raises on detection failure. The
5
+ `register` command turns "essential field is None" into a user-facing
6
+ error; the helpers themselves are pure.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import shutil
12
+ import socket
13
+ import subprocess
14
+ import uuid
15
+ from pathlib import Path
16
+
17
+ _MACHINE_ID_PATHS: list[Path] = [
18
+ Path("/etc/machine-id"),
19
+ Path("/var/lib/dbus/machine-id"),
20
+ ]
21
+
22
+
23
+ def detect_hostname() -> str | None:
24
+ """Return the box's hostname, or None if empty."""
25
+ name = socket.gethostname()
26
+ return name or None
27
+
28
+
29
+ def detect_lan_ip() -> str | None:
30
+ """Return the IP of the interface that routes to the public internet.
31
+
32
+ Uses the UDP-socket trick: connecting a SOCK_DGRAM socket to a public
33
+ address resolves the route locally without sending a packet. The
34
+ socket's local address is the LAN IP that would carry outgoing
35
+ traffic. Returns None on any OSError (no route, no network, etc.).
36
+ """
37
+ try:
38
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
39
+ s.connect(("8.8.8.8", 80))
40
+ return s.getsockname()[0]
41
+ except OSError:
42
+ return None
43
+
44
+
45
+ def resolve_machine_identity(
46
+ *, fallback_path: Path | None = None
47
+ ) -> str:
48
+ """Return a stable identifier for this box, creating one if needed.
49
+
50
+ Get-or-create, not pure detection: tries /etc/machine-id, then
51
+ /var/lib/dbus/machine-id, and as a last resort **writes** a UUID to
52
+ fallback_path (default ~/.lablink/machine_identity) so the value is
53
+ stable across reboots even on systems without machine-id (e.g.,
54
+ older distributions, custom containers). Callers that only want to
55
+ inspect — without persisting anything — should read the candidate
56
+ paths directly.
57
+ """
58
+ for path in _MACHINE_ID_PATHS:
59
+ try:
60
+ content = path.read_text().strip()
61
+ except OSError:
62
+ continue
63
+ if content:
64
+ return content
65
+
66
+ if fallback_path is None:
67
+ fallback_path = Path.home() / ".lablink" / "machine_identity"
68
+
69
+ if fallback_path.exists():
70
+ existing = fallback_path.read_text().strip()
71
+ if existing:
72
+ return existing
73
+
74
+ value = uuid.uuid4().hex
75
+ fallback_path.parent.mkdir(parents=True, exist_ok=True)
76
+ fallback_path.write_text(value)
77
+ return value
78
+
79
+
80
+ def detect_gpu() -> tuple[bool, str | None]:
81
+ """Return (present, model) by invoking `nvidia-smi -L`.
82
+
83
+ Returns (False, None) if nvidia-smi is missing, returns non-zero, or
84
+ its output doesn't parse. Model is extracted from the first line of
85
+ `nvidia-smi -L` (typical format: "GPU 0: NVIDIA T4 (UUID: ...)").
86
+ Never raises.
87
+ """
88
+ if shutil.which("nvidia-smi") is None:
89
+ return (False, None)
90
+ try:
91
+ result = subprocess.run(
92
+ ["nvidia-smi", "-L"],
93
+ capture_output=True,
94
+ text=True,
95
+ timeout=5,
96
+ )
97
+ except (OSError, subprocess.SubprocessError):
98
+ return (False, None)
99
+ if result.returncode != 0:
100
+ return (False, None)
101
+ first = (result.stdout or "").splitlines()[0:1]
102
+ if not first:
103
+ return (False, None)
104
+ # "GPU 0: NVIDIA T4 (UUID: GPU-xxx)" -> "NVIDIA T4"
105
+ line = first[0]
106
+ after_colon = line.split(":", 1)
107
+ if len(after_colon) != 2:
108
+ return (False, None)
109
+ model = after_colon[1].split("(")[0].strip()
110
+ if not model:
111
+ return (False, None)
112
+ return (True, model)
File without changes