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:
parent
bf26fc881a
commit
8e2fe83802
11 changed files with 125 additions and 188 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
*.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Real credentials must never be committed — use the .example files
|
||||||
|
archinstall/user_credentials.json
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
pip install psutil
|
|
||||||
sudo apt-get install smartmontools
|
|
||||||
146
driveval/main.py
146
driveval/main.py
|
|
@ -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()
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
psutil
|
|
||||||
2
webinstaller/.gitignore
vendored
2
webinstaller/.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
||||||
*.venv/
|
|
||||||
*__pycache__*
|
|
||||||
|
|
@ -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
104
webinstaller/drives.py
Normal 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()
|
||||||
|
|
@ -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")
|
|
||||||
1
webinstaller/requirements.txt
Normal file
1
webinstaller/requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
flask
|
||||||
|
|
@ -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:
|
||||||
{% endfor %}
|
<select name="boot_drive_uuid" required>
|
||||||
<form method="post", action="{{ url_for('install_step_2') }}">
|
{% for d in drives %}
|
||||||
<p>Boot Drive: <input type="text" name="boot_drive_uuid" required /></p>
|
<option value="{{ d.name }}">{{ d.name }} ({{ d.size }}, score {{ d.score }})</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
<input type="submit" value="Go to Overview" />
|
<input type="submit" value="Go to Overview" />
|
||||||
</form>
|
</form>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue