From bf26fc881ac9fa7e840faa001eb661a3d990a46a Mon Sep 17 00:00:00 2001 From: Robert Syrnicki Date: Mon, 13 Apr 2026 19:38:34 +0200 Subject: [PATCH] basics and hardware eval --- archinstall/user_configuration.json | 55 +++++++ archinstall/user_credentials.json | 9 ++ driveval/dependancies.txt | 2 + driveval/main.py | 146 +++++++++++++++++++ driveval/requirements.txt | 1 + webinstaller/.gitignore | 2 + webinstaller/app.py | 50 +++++++ webinstaller/hardware.py | 23 +++ webinstaller/static/style.css | 0 webinstaller/templates/base.html | 0 webinstaller/templates/install/overview.html | 13 ++ webinstaller/templates/install/step1.html | 13 ++ webinstaller/templates/install/step2.html | 16 ++ 13 files changed, 330 insertions(+) create mode 100644 archinstall/user_configuration.json create mode 100644 archinstall/user_credentials.json create mode 100644 driveval/dependancies.txt create mode 100644 driveval/main.py create mode 100644 driveval/requirements.txt create mode 100644 webinstaller/.gitignore create mode 100644 webinstaller/app.py create mode 100644 webinstaller/hardware.py create mode 100644 webinstaller/static/style.css create mode 100644 webinstaller/templates/base.html create mode 100644 webinstaller/templates/install/overview.html create mode 100644 webinstaller/templates/install/step1.html create mode 100644 webinstaller/templates/install/step2.html diff --git a/archinstall/user_configuration.json b/archinstall/user_configuration.json new file mode 100644 index 0000000..b70a688 --- /dev/null +++ b/archinstall/user_configuration.json @@ -0,0 +1,55 @@ +{ + "archinstall-language": "English", + "timezone": "Europe/Berlin", + "ntp": true, + + "bootloader": "Systemd-boot", + + "disk_config": { + "config_type": "use_entire_disk", + "device": "/dev/sda", + "filesystem": "ext4" + }, + + "hostname": "arch-server", + + "kernels": ["linux"], + + "packages": [ + "docker", + "docker-compose", + "vim", + "git", + "htop", + "curl" + ], + + "profile": { + "type": "server" + }, + + "services": [ + "docker" + ], + + "network_config": { + "type": "iso" + }, + + "users": [ + { + "username": "server", + "sudo": true, + "groups": ["docker"] + } + ], + + "ssh": true, + + "audio_config": null, + + "locale_config": { + "locale": "en_US.UTF-8", + "keyboard_layout": "us" + } +} \ No newline at end of file diff --git a/archinstall/user_credentials.json b/archinstall/user_credentials.json new file mode 100644 index 0000000..cc8cc0a --- /dev/null +++ b/archinstall/user_credentials.json @@ -0,0 +1,9 @@ +{ + "root_password": "CHANGE_ME", + "users": [ + { + "username": "server", + "password": "CHANGE_ME" + } + ] +} \ No newline at end of file diff --git a/driveval/dependancies.txt b/driveval/dependancies.txt new file mode 100644 index 0000000..c7b8ca6 --- /dev/null +++ b/driveval/dependancies.txt @@ -0,0 +1,2 @@ +pip install psutil +sudo apt-get install smartmontools \ No newline at end of file diff --git a/driveval/main.py b/driveval/main.py new file mode 100644 index 0000000..382fd95 --- /dev/null +++ b/driveval/main.py @@ -0,0 +1,146 @@ +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() \ No newline at end of file diff --git a/driveval/requirements.txt b/driveval/requirements.txt new file mode 100644 index 0000000..0b574b5 --- /dev/null +++ b/driveval/requirements.txt @@ -0,0 +1 @@ +psutil \ No newline at end of file diff --git a/webinstaller/.gitignore b/webinstaller/.gitignore new file mode 100644 index 0000000..4362d8b --- /dev/null +++ b/webinstaller/.gitignore @@ -0,0 +1,2 @@ +*.venv/ +*__pycache__* diff --git a/webinstaller/app.py b/webinstaller/app.py new file mode 100644 index 0000000..5cd5cc9 --- /dev/null +++ b/webinstaller/app.py @@ -0,0 +1,50 @@ +from flask import Flask, render_template, request, redirect, url_for +from hardware import get_hardware_info + +app = Flask(__name__) + +settings = { + # Step 1 + "hostname": "furtka", + "username": "", + "password": "", + "password2": "", + "backend": False, + "backend_adress": "127.0.0.1", + "language" + # devices + "boot_drive_uuid": "1" +} + +@app.route("/") +def home(): + return "Hello World" + +@app.route("/install/step1", methods=["GET", "POST"]) +def install_step_1(): + if request.method == "POST": + settings["hostname"] = request.form["hostname"] + + return redirect(url_for("install_step_2")) + + return render_template("install/step1.html") + + +@app.route("/install/step2", methods=["GET", "POST"]) +def install_step_2(): + if request.method == "POST": + settings["boot_drive_uuid"] = request.form["boot_drive_uuid"] + + return redirect(url_for("install_overview")) + + return render_template("install/step2.html", storage=get_hardware_info("storage")) + + +@app.route("/install/overview") +def install_overview(): + + return render_template("install/overview.html", settings=settings) + + +if __name__ == "__main__": + app.run(debug=True, port=5000) diff --git a/webinstaller/hardware.py b/webinstaller/hardware.py new file mode 100644 index 0000000..cebb3dd --- /dev/null +++ b/webinstaller/hardware.py @@ -0,0 +1,23 @@ +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") \ No newline at end of file diff --git a/webinstaller/static/style.css b/webinstaller/static/style.css new file mode 100644 index 0000000..e69de29 diff --git a/webinstaller/templates/base.html b/webinstaller/templates/base.html new file mode 100644 index 0000000..e69de29 diff --git a/webinstaller/templates/install/overview.html b/webinstaller/templates/install/overview.html new file mode 100644 index 0000000..69b3c26 --- /dev/null +++ b/webinstaller/templates/install/overview.html @@ -0,0 +1,13 @@ + + + + Furtka Install + + +

Overview

+

Results:

+ {% for k, s in settings.items() %} +

{{k}}: {{s}}

+ {% endfor %} + + diff --git a/webinstaller/templates/install/step1.html b/webinstaller/templates/install/step1.html new file mode 100644 index 0000000..9daa3ed --- /dev/null +++ b/webinstaller/templates/install/step1.html @@ -0,0 +1,13 @@ + + + + Furtka Install + + +

Step 1

+
+

Hostname:

+ +
+ + diff --git a/webinstaller/templates/install/step2.html b/webinstaller/templates/install/step2.html new file mode 100644 index 0000000..383ff6e --- /dev/null +++ b/webinstaller/templates/install/step2.html @@ -0,0 +1,16 @@ + + + + Furtka Install + + +

Step 2 - Choose Boot Drive

+ {% for h in storage %} +

{{ h }}

+ {% endfor %} +
+

Boot Drive:

+ +
+ +