Skip to content

Commit a0b734b

Browse files
mdshakib007sazid
andauthored
Added system deps for CfT into installer code (#646)
* added system deps for cft into installar code for zeuz node. * Add check for ubuntu >= 24.04 * Fix sudo password askign everytime without checking what's installed in the system * Cache unavailable packages so we don't ask for password from user everytime node runs. --------- Co-authored-by: sazid <sazidz@outlook.com>
1 parent 8f2a2d1 commit a0b734b

2 files changed

Lines changed: 536 additions & 145 deletions

File tree

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import os
2+
import re
3+
import shutil
4+
import subprocess
5+
from pathlib import Path
6+
7+
8+
class LinuxSystemHelper:
9+
def __init__(self, unavailable_cache_file=None):
10+
self.os_release = self._load_os_release()
11+
self.unavailable_cache_file = (
12+
Path(unavailable_cache_file) if unavailable_cache_file else None
13+
)
14+
15+
def _load_os_release(self):
16+
data = {}
17+
try:
18+
with open("/etc/os-release", "r") as f:
19+
for line in f:
20+
line = line.strip()
21+
if not line or "=" not in line:
22+
continue
23+
key, value = line.split("=", 1)
24+
data[key] = value.strip().strip('"').strip("'")
25+
except Exception:
26+
return {}
27+
return data
28+
29+
def is_ubuntu_version_at_least(self, major, minor=0):
30+
distro_id = self.os_release.get("ID", "").lower()
31+
version_id = self.os_release.get("VERSION_ID", "")
32+
33+
version_parts = version_id.split(".")
34+
current_major = (
35+
int(version_parts[0])
36+
if len(version_parts) > 0 and version_parts[0].isdigit()
37+
else 0
38+
)
39+
current_minor = (
40+
int(version_parts[1])
41+
if len(version_parts) > 1 and version_parts[1].isdigit()
42+
else 0
43+
)
44+
45+
return distro_id == "ubuntu" and (current_major, current_minor) >= (
46+
major,
47+
minor,
48+
)
49+
50+
def is_gui_environment(self):
51+
return bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"))
52+
53+
def is_gnome_session(self):
54+
desktop = os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
55+
session = os.environ.get("DESKTOP_SESSION", "").lower()
56+
return "gnome" in desktop or "gnome" in session
57+
58+
def supports_sudo_askpass(self):
59+
if not shutil.which("sudo"):
60+
return False
61+
62+
try:
63+
result = subprocess.run(
64+
["sudo", "-h"],
65+
capture_output=True,
66+
text=True,
67+
check=False,
68+
)
69+
help_output = f"{result.stdout}\n{result.stderr}".lower()
70+
return "askpass" in help_output and "-a" in help_output
71+
except Exception:
72+
return False
73+
74+
def get_privilege_escalation_command(self):
75+
if (
76+
self.is_gui_environment()
77+
and self.is_gnome_session()
78+
and shutil.which("pkexec")
79+
):
80+
return ["pkexec"], "pkexec"
81+
82+
askpass = os.environ.get("SUDO_ASKPASS", "")
83+
if (
84+
self.is_gui_environment()
85+
and self.supports_sudo_askpass()
86+
and askpass
87+
and Path(askpass).exists()
88+
and shutil.which("sudo")
89+
):
90+
return ["sudo", "-A"], "sudo -A"
91+
92+
return ["sudo"], "sudo"
93+
94+
def _is_package_available(self, package):
95+
try:
96+
result = subprocess.run(
97+
["apt-cache", "policy", package],
98+
capture_output=True,
99+
text=True,
100+
check=False,
101+
)
102+
if result.returncode != 0:
103+
return False
104+
105+
output = f"{result.stdout}\n{result.stderr}".lower()
106+
if "candidate:" not in output:
107+
return False
108+
109+
for line in output.splitlines():
110+
if line.strip().startswith("candidate:"):
111+
candidate = line.split(":", 1)[1].strip()
112+
return candidate != "(none)"
113+
114+
return False
115+
except Exception:
116+
return False
117+
118+
def _pick_available_package(self, package_options):
119+
for package_name in package_options:
120+
if self._is_package_available(package_name):
121+
return package_name
122+
return None
123+
124+
def get_chrome_dependency_packages(self):
125+
dependencies = [
126+
["libnss3"],
127+
["libxss1"],
128+
["libappindicator3-1"],
129+
["fonts-liberation"],
130+
["libasound2t64", "libasound2"],
131+
["libnspr4"],
132+
["libx11-xcb1"],
133+
["libxcomposite1"],
134+
["libxcursor1"],
135+
["libxdamage1"],
136+
["libxi6"],
137+
["libxtst6"],
138+
["libglib2.0-0t64", "libglib2.0-0"],
139+
["libgtk-3-0t64", "libgtk-3-0"],
140+
["libgdk-pixbuf2.0-0", "libgdk-pixbuf-xlib-2.0-0"],
141+
["libxrandr2"],
142+
["libpangocairo-1.0-0"],
143+
["libatk1.0-0t64", "libatk1.0-0"],
144+
["libcairo-gobject2"],
145+
["xvfb"],
146+
["ca-certificates"],
147+
["libatk-bridge2.0-0t64", "libatk-bridge2.0-0"],
148+
["libdrm2"],
149+
["libxkbcommon0"],
150+
["lsb-release"],
151+
["wget"],
152+
["xdg-utils"],
153+
]
154+
155+
selected_packages = []
156+
for package_group in dependencies:
157+
selected = self._pick_available_package(package_group)
158+
if selected:
159+
selected_packages.append(selected)
160+
else:
161+
print(
162+
"Warning: Could not resolve package from options: "
163+
f"{', '.join(package_group)}"
164+
)
165+
166+
return selected_packages
167+
168+
def _is_package_installed(self, package):
169+
try:
170+
result = subprocess.run(
171+
["dpkg-query", "-W", "-f=${Status}", package],
172+
capture_output=True,
173+
text=True,
174+
check=False,
175+
)
176+
if result.returncode != 0:
177+
return False
178+
return "install ok installed" in result.stdout.lower()
179+
except Exception:
180+
return False
181+
182+
def get_missing_packages(self, packages):
183+
return [
184+
package for package in packages if not self._is_package_installed(package)
185+
]
186+
187+
def _load_unavailable_packages(self):
188+
if not self.unavailable_cache_file or not self.unavailable_cache_file.exists():
189+
return set()
190+
191+
try:
192+
with open(self.unavailable_cache_file, "r") as f:
193+
return {line.strip() for line in f if line.strip()}
194+
except Exception:
195+
return set()
196+
197+
def _save_unavailable_packages(self, packages):
198+
if not self.unavailable_cache_file:
199+
return
200+
201+
try:
202+
self.unavailable_cache_file.parent.mkdir(parents=True, exist_ok=True)
203+
with open(self.unavailable_cache_file, "w") as f:
204+
for package in sorted(packages):
205+
f.write(f"{package}\n")
206+
except Exception:
207+
return
208+
209+
def add_unavailable_packages(self, packages):
210+
package_set = {pkg for pkg in packages if pkg}
211+
if not package_set:
212+
return set()
213+
214+
existing = self._load_unavailable_packages()
215+
updated = existing | package_set
216+
newly_added = updated - existing
217+
if newly_added:
218+
self._save_unavailable_packages(updated)
219+
return newly_added
220+
221+
def filter_cached_unavailable_packages(self, packages):
222+
cached = self._load_unavailable_packages()
223+
allowed = [package for package in packages if package not in cached]
224+
skipped = [package for package in packages if package in cached]
225+
return allowed, skipped
226+
227+
def get_cached_unavailable_packages(self):
228+
return sorted(self._load_unavailable_packages())
229+
230+
def extract_unavailable_packages_from_apt_output(self, output):
231+
patterns = [
232+
r"E:\s+Package '([^']+)' has no installation candidate",
233+
r"E:\s+Unable to locate package\s+(\S+)",
234+
r"Package\s+(\S+)\s+is not available, but is referred to by another package",
235+
]
236+
237+
unavailable = set()
238+
for pattern in patterns:
239+
unavailable.update(re.findall(pattern, output))
240+
241+
return sorted(unavailable)

0 commit comments

Comments
 (0)