feat: install progress bar + fix docker group creation order
Two tangled changes to the install flow, batched because they're both
small and hit app.py:
1. Phase-based progress bar on /install/log. parse_install_progress()
scans the archinstall log for ordered phase markers ("Wiping
partitions", "Installing packages: ['base'", "Adding bootloader",
"Installation completed without any errors", …) and exposes
percent + user-facing phase label + status (running/done/error).
Template wraps the raw log in a collapsed <details> so the default
view stays calm; the meta-refresh stops once status is terminal.
If archinstall changes its stdout wording the bar stalls on the
last recognized phase — the install itself is unaffected.
2. Drop "docker" from the user's groups in creds and do the
`gpasswd -a <user> docker` via custom_commands instead.
archinstall creates users before pacstrapping the extras list, so
the docker group doesn't exist at user-create time —
caused the second real install to crash with
`gpasswd: group 'docker' does not exist`. custom_commands runs
at the very end, after docker is installed. Username is validated
by USERNAME_RE so no shell injection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
51cdf460d9
commit
3a259beb98
3 changed files with 112 additions and 6 deletions
|
|
@ -33,6 +33,46 @@ settings = {
|
||||||
HOSTNAME_RE = re.compile(r"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$")
|
HOSTNAME_RE = re.compile(r"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$")
|
||||||
USERNAME_RE = re.compile(r"^[a-z_][a-z0-9_-]{0,31}$")
|
USERNAME_RE = re.compile(r"^[a-z_][a-z0-9_-]{0,31}$")
|
||||||
|
|
||||||
|
# Ordered phase markers for the install progress bar. Each tuple is
|
||||||
|
# (substring to search for in the archinstall log, progress percent when
|
||||||
|
# reached, user-facing label). Pick the furthest phase whose marker is
|
||||||
|
# present in the log. If archinstall changes its stdout wording the bar
|
||||||
|
# stalls on the last recognized phase — the install itself keeps going.
|
||||||
|
PROGRESS_PHASES = [
|
||||||
|
("Wiping partitions", 8, "Preparing your disk"),
|
||||||
|
("Creating partitions", 12, "Creating partitions"),
|
||||||
|
("Starting installation", 15, "Starting installation"),
|
||||||
|
("Waiting for", 18, "Syncing time and packages"),
|
||||||
|
("Installing packages: ['base'", 25, "Installing the base system (this takes a while)"),
|
||||||
|
("Adding bootloader", 65, "Setting up boot"),
|
||||||
|
("Installing packages: ['efibootmgr'", 70, "Setting up boot"),
|
||||||
|
("Installing packages: ['docker'", 80, "Installing your apps"),
|
||||||
|
("Enabling service", 90, "Turning on services"),
|
||||||
|
("Updating /mnt/etc/fstab", 95, "Almost done"),
|
||||||
|
("Installation completed without any errors", 100, "Done!"),
|
||||||
|
]
|
||||||
|
|
||||||
|
PROGRESS_ERROR_MARKERS = ("Traceback (most recent call last)", "archinstall: error:")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_install_progress(log):
|
||||||
|
percent = 2
|
||||||
|
phase = "Starting up…"
|
||||||
|
for marker, pct, label in PROGRESS_PHASES:
|
||||||
|
if marker in log:
|
||||||
|
percent = pct
|
||||||
|
phase = label
|
||||||
|
|
||||||
|
if percent >= 100:
|
||||||
|
status = "done"
|
||||||
|
elif any(m in log for m in PROGRESS_ERROR_MARKERS):
|
||||||
|
status = "error"
|
||||||
|
phase = "Installation failed — open Show details below"
|
||||||
|
else:
|
||||||
|
status = "running"
|
||||||
|
|
||||||
|
return {"percent": percent, "phase": phase, "status": status}
|
||||||
|
|
||||||
|
|
||||||
def validate_step1(form):
|
def validate_step1(form):
|
||||||
errors = []
|
errors = []
|
||||||
|
|
@ -102,6 +142,11 @@ def build_archinstall_config(s):
|
||||||
"packages": ["docker", "docker-compose", "vim", "git", "htop", "curl"],
|
"packages": ["docker", "docker-compose", "vim", "git", "htop", "curl"],
|
||||||
"profile": {"type": "server"},
|
"profile": {"type": "server"},
|
||||||
"services": ["docker"],
|
"services": ["docker"],
|
||||||
|
# Add user to the docker group post-install. We can't put "docker" in
|
||||||
|
# the user's `groups` at create-time because archinstall creates users
|
||||||
|
# before pacstrapping the extras, so the docker group doesn't exist
|
||||||
|
# yet. custom_commands runs at the very end.
|
||||||
|
"custom_commands": [f"gpasswd -a {s['username']} docker"],
|
||||||
"network_config": {"type": "iso"},
|
"network_config": {"type": "iso"},
|
||||||
"ssh": True,
|
"ssh": True,
|
||||||
"audio_config": None,
|
"audio_config": None,
|
||||||
|
|
@ -123,7 +168,7 @@ def build_archinstall_creds(s):
|
||||||
"username": s["username"],
|
"username": s["username"],
|
||||||
"!password": s["password"],
|
"!password": s["password"],
|
||||||
"sudo": True,
|
"sudo": True,
|
||||||
"groups": ["docker"],
|
"groups": [],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
@ -216,8 +261,12 @@ def install_run():
|
||||||
|
|
||||||
@app.route("/install/log")
|
@app.route("/install/log")
|
||||||
def install_log_view():
|
def install_log_view():
|
||||||
log = INSTALL_LOG.read_text() if INSTALL_LOG.exists() else "(waiting for install to start)\n"
|
log = INSTALL_LOG.read_text() if INSTALL_LOG.exists() else ""
|
||||||
return render_template("install/log.html", log=log)
|
return render_template(
|
||||||
|
"install/log.html",
|
||||||
|
log=log,
|
||||||
|
progress=parse_install_progress(log),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -337,6 +337,45 @@ select:focus {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
height: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 1.5rem 0 0.5rem;
|
||||||
|
}
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--accent),
|
||||||
|
color-mix(in srgb, var(--accent) 55%, #fff)
|
||||||
|
);
|
||||||
|
transition: width 0.6s ease;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
.progress-bar-error { background: var(--danger); }
|
||||||
|
.progress-bar-done { background: var(--success); }
|
||||||
|
.progress-phase {
|
||||||
|
margin: 0.3rem 0 1.5rem;
|
||||||
|
color: var(--fg-muted);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-details {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
.log-details summary {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--fg-muted);
|
||||||
|
user-select: none;
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
}
|
||||||
|
.log-details summary:hover { color: var(--fg); }
|
||||||
|
.log-details[open] summary { margin-bottom: 0.6rem; }
|
||||||
|
|
||||||
/* ── Footer ─────────────────────────────────────────────────── */
|
/* ── Footer ─────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.site-footer {
|
.site-footer {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,30 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Installing… · Furtka Installer{% endblock %}
|
{% block title %}Installing… · Furtka Installer{% endblock %}
|
||||||
{% block head_extra %}<meta http-equiv="refresh" content="3">{% endblock %}
|
{% block head_extra %}
|
||||||
|
{% if progress.status == "running" %}<meta http-equiv="refresh" content="3">{% endif %}
|
||||||
|
{% endblock %}
|
||||||
{% block step_indicator %}<span class="step-indicator">Installing</span>{% endblock %}
|
{% block step_indicator %}<span class="step-indicator">Installing</span>{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% if progress.status == "done" %}
|
||||||
|
<h1>Furtka is ready</h1>
|
||||||
|
<p class="lede">Installation finished. Remove the USB / eject the installer image, then reboot.</p>
|
||||||
|
{% elif progress.status == "error" %}
|
||||||
|
<h1>Installation hit a snag</h1>
|
||||||
|
<p class="lede">Something went wrong. Open the details below and share them so we can help.</p>
|
||||||
|
{% else %}
|
||||||
<h1>Installing Furtka</h1>
|
<h1>Installing Furtka</h1>
|
||||||
<p class="lede">This page reloads every 3 seconds. Don't close it. Don't power off.</p>
|
<p class="lede">This takes a few minutes. Don't close this page or power off the machine.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<pre class="log">{{ log }}</pre>
|
<div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="{{ progress.percent }}">
|
||||||
|
<div class="progress-bar{% if progress.status == 'error' %} progress-bar-error{% endif %}{% if progress.status == 'done' %} progress-bar-done{% endif %}" style="width: {{ progress.percent }}%;"></div>
|
||||||
|
</div>
|
||||||
|
<p class="progress-phase">{{ progress.phase }} · {{ progress.percent }}%</p>
|
||||||
|
|
||||||
|
<details class="log-details">
|
||||||
|
<summary>Show details</summary>
|
||||||
|
<pre class="log">{{ log or "(waiting for install to start)" }}</pre>
|
||||||
|
</details>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue