Fix installer basics: syntax, secrets, consolidation

- Untrack archinstall/user_credentials.json; ship .example file and
  add a root .gitignore so real creds stay out of git
- Fix SyntaxError in webinstaller/app.py (malformed "language" entry)
- Drop import-time lshw call in hardware.py
- Consolidate driveval/ and webinstaller/hardware.py into a single
  webinstaller/drives.py with a list_scored_devices() API; step 2
  now renders a proper <select> with name/size/score
- Replace dependancies.txt typo with webinstaller/requirements.txt
  (Flask only — psutil was imported but unused)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Daniel Maksymilian Syrnicki 2026-04-13 19:44:29 +02:00
parent bf26fc881a
commit 8e2fe83802
11 changed files with 125 additions and 188 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
*.venv/
__pycache__/
*.pyc
# Real credentials must never be committed — use the .example files
archinstall/user_credentials.json

View file

@ -1,2 +0,0 @@
pip install psutil
sudo apt-get install smartmontools

View file

@ -1,146 +0,0 @@
import psutil
import subprocess
def get_drive_health(device):
"""
Get the health of a storage device using smartctl.
Returns a score based on the drive's SMART health status.
"""
try:
result = subprocess.run(['smartctl', '-H', device], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output = result.stdout.decode()
if "PASSED" in output:
return 10 # Healthy drive
elif "FAILED" in output:
return 0 # Failed drive
else:
return 5 # Unknown or problematic drive
except Exception as e:
print(f"Error checking SMART status for {device}: {e}")
return 5 # Default score for uncheckable devices
def get_drive_type(device):
"""
Determine if a device is an SSD or HDD based on its device type.
"""
if 'NVME' in device:
return 15 # SSDs are optimal
elif 'SSD' in device:
return 10 # SSDs are optimal
else:
return 5 # HDDs are less optimal for boot drives
def get_drive_size(device):
"""
Get size of a block device using lsblk (works for disks).
Always returns an integer score.
"""
try:
result = subprocess.run(
["lsblk", "-dn", "-o", "SIZE", device],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True
)
size_str = result.stdout.strip().upper()
if not size_str:
return 5 # fallback
size_str = size_str.replace(",", ".")
# Convert to GB
if size_str.endswith("T"):
size_gb = float(size_str[:-1]) * 1024
elif size_str.endswith("G"):
size_gb = float(size_str[:-1])
elif size_str.endswith("M"):
size_gb = float(size_str[:-1]) / 1024
else:
return 5 # unknown format
if size_gb < 128:
return 5
elif size_gb < 512:
return 7
else:
return 10
except Exception as e:
print(f"Error getting size for {device}: {e}")
return 5 # ALWAYS return something
def get_device_score(device):
"""
Calculate a suitability score for each drive based on:
- Type (SSD/HDD)
- Health (SMART status)
- Size (GB)
"""
score = 0
# Type
score += get_drive_type(device)
# Health
score += get_drive_health(device)
# Size
score += get_drive_size(device)
return score
def list_storage_devices():
"""
List all physical storage devices (e.g. /dev/sda, /dev/nvme0n1)
and return them with their computed scores.
Compatible with the existing scoring pipeline.
"""
devices = []
try:
result = subprocess.run(
["lsblk", "-dn", "-o", "NAME"], # -d = disks only, no partitions
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True
)
for line in result.stdout.strip().split("\n"):
if not line:
continue
device = f"/dev/{line.strip()}"
score = get_device_score(device) # <-- reuses your existing logic
devices.append((device, score))
except subprocess.CalledProcessError as e:
print(f"Error listing devices: {e}")
return devices
def main():
devices = list_storage_devices()
print(devices)
if not devices:
print("No storage devices found.")
return
print(f"\n{'Device':<20} {'Score'}")
print("-" * 30)
for device, score in devices:
print(f"{device:<20} {score}")
# Find the highest scoring drive
best_device = max(devices, key=lambda x: x[1])
print(f"\nBest drive for boot: {best_device[0]} with score: {best_device[1]}")
if __name__ == "__main__":
main()

View file

@ -1 +0,0 @@
psutil

View file

@ -1,2 +0,0 @@
*.venv/
*__pycache__*

View file

@ -1,5 +1,5 @@
from flask import Flask, render_template, request, redirect, url_for from flask import Flask, render_template, request, redirect, url_for
from hardware import get_hardware_info from drives import list_scored_devices
app = Flask(__name__) app = Flask(__name__)
@ -11,22 +11,22 @@ settings = {
"password2": "", "password2": "",
"backend": False, "backend": False,
"backend_adress": "127.0.0.1", "backend_adress": "127.0.0.1",
"language" "language": "en",
# devices # devices
"boot_drive_uuid": "1" "boot_drive_uuid": "",
} }
@app.route("/") @app.route("/")
def home(): def home():
return "Hello World" return "Hello World"
@app.route("/install/step1", methods=["GET", "POST"]) @app.route("/install/step1", methods=["GET", "POST"])
def install_step_1(): def install_step_1():
if request.method == "POST": if request.method == "POST":
settings["hostname"] = request.form["hostname"] settings["hostname"] = request.form["hostname"]
return redirect(url_for("install_step_2")) return redirect(url_for("install_step_2"))
return render_template("install/step1.html") return render_template("install/step1.html")
@ -34,15 +34,12 @@ def install_step_1():
def install_step_2(): def install_step_2():
if request.method == "POST": if request.method == "POST":
settings["boot_drive_uuid"] = request.form["boot_drive_uuid"] settings["boot_drive_uuid"] = request.form["boot_drive_uuid"]
return redirect(url_for("install_overview")) return redirect(url_for("install_overview"))
return render_template("install/step2.html", drives=list_scored_devices())
return render_template("install/step2.html", storage=get_hardware_info("storage"))
@app.route("/install/overview") @app.route("/install/overview")
def install_overview(): def install_overview():
return render_template("install/overview.html", settings=settings) return render_template("install/overview.html", settings=settings)

104
webinstaller/drives.py Normal file
View file

@ -0,0 +1,104 @@
import subprocess
def get_drive_health(device):
try:
result = subprocess.run(
["smartctl", "-H", device],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
output = result.stdout.decode()
if "PASSED" in output:
return 10
elif "FAILED" in output:
return 0
return 5
except Exception as e:
print(f"Error checking SMART status for {device}: {e}")
return 5
def get_drive_type_score(device):
name = device.lower()
if "nvme" in name:
return 15
if "ssd" in name:
return 10
return 5
def parse_size_gb(size_str):
size_str = size_str.strip().upper().replace(",", ".")
if not size_str:
return None
if size_str.endswith("T"):
return float(size_str[:-1]) * 1024
if size_str.endswith("G"):
return float(size_str[:-1])
if size_str.endswith("M"):
return float(size_str[:-1]) / 1024
return None
def get_size_score(size_gb):
if size_gb is None:
return 5
if size_gb < 128:
return 5
if size_gb < 512:
return 7
return 10
def score_device(device, size_gb):
return get_drive_type_score(device) + get_drive_health(device) + get_size_score(size_gb)
def list_scored_devices():
"""Return [{name, size, score}, ...] for all physical disks, highest score first."""
devices = []
try:
result = subprocess.run(
["lsblk", "-dn", "-o", "NAME,SIZE"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True,
)
for line in result.stdout.strip().split("\n"):
if not line:
continue
parts = line.split()
if len(parts) < 2:
continue
name, size = parts[0], parts[1]
device = f"/dev/{name}"
devices.append(
{
"name": device,
"size": size,
"score": score_device(device, parse_size_gb(size)),
}
)
except subprocess.CalledProcessError as e:
print(f"Error listing devices: {e}")
devices.sort(key=lambda d: d["score"], reverse=True)
return devices
def main():
devices = list_scored_devices()
if not devices:
print("No storage devices found.")
return
print(f"\n{'Device':<20} {'Size':<10} {'Score'}")
print("-" * 40)
for d in devices:
print(f"{d['name']:<20} {d['size']:<10} {d['score']}")
print(f"\nBest drive for boot: {devices[0]['name']} (score {devices[0]['score']})")
if __name__ == "__main__":
main()

View file

@ -1,23 +0,0 @@
from os import popen
import json
class HardwareDevice:
def __init__(self, hw_path, device, device_class, description):
self.hw_path = hw_path
self.device = device
self.device_class = device_class
self.description = description
def __str__(self):
return f"{self.description}@{self.device}"
def get_hardware_info(hw_type: str):
hardware_read_process = popen(f"lshw -json -c {hw_type}")
hardware = json.loads(hardware_read_process.read())
hardware_read_process.close()
for hw in hardware:
print(hw["description"])
return hardware
get_hardware_info(hw_type="storage")

View file

@ -0,0 +1 @@
flask

View file

@ -5,11 +5,14 @@
</head> </head>
<body> <body>
<h1>Step 2 - Choose Boot Drive</h1> <h1>Step 2 - Choose Boot Drive</h1>
{% for h in storage %} <form method="post" action="{{ url_for('install_step_2') }}">
<p>{{ h }}</p> <p>Boot Drive:
<select name="boot_drive_uuid" required>
{% for d in drives %}
<option value="{{ d.name }}">{{ d.name }} ({{ d.size }}, score {{ d.score }})</option>
{% endfor %} {% endfor %}
<form method="post", action="{{ url_for('install_step_2') }}"> </select>
<p>Boot Drive: <input type="text" name="boot_drive_uuid" required /></p> </p>
<input type="submit" value="Go to Overview" /> <input type="submit" value="Go to Overview" />
</form> </form>
</body> </body>