Everything West Jefferson specific moved out to a separate repo (pxe-site-wj): NTLARS/UDC/heat-treat backups keyed by WJ machine numbers, CMM and Wax/Trace bay lists, WJ OpenText profiles, site-config.json, the WJ preinstall list, and the WJ shopfloor PC types (Collections, NoCollections, Common, Keyence, CMM, Genspect, HeatTreat, Wax/Trace, Display, PartMarker) with GE-Enforce, machine numbers and the desktop layout. The part-marker bench tools moved to their own repo. Dead scripts removed: build-proxmox-iso, deploy-bios, sync_hardware_models, mirror-from-gold, pull-bios, startnet-template.cmd. Shopfloor is one generic type, gea-shopfloor-standard, with today's timing: - PPKG at first logon (unattend Order 10, unchanged). - Install-AppList.ps1 runs C:\PreInstall\preinstall.json during setup. - A new SYSTEM task, GE Shopfloor Post-PPKG, runs postppkg.json once after the post-PPKG reboot. It also clears AutoLogonCount after the ShopFloor handover (the Users-level monitor can be denied that HKLM write) and removes the Intune Sync task when done. - Both lists ship empty. A site adds entries and installers. - WinPE choice 3 goes straight to the PPKG menu. Staging, fetch and heal carry only the generic payload. The webapp's shopfloor sub-menu editor is gone, and the imaging tiles name the new stages. - The playbook deploys the empty lists only if missing. It now deploys the PowerShell 7 MSI the shopfloor unattend installs, which it never did before. Blancco site settings move into the webapp (Updates > Blancco releases): business location, licensing Wi-Fi SSID and password. They are rendered into the served preferences.xml and applied to every staged release, and are seeded from site-vars (site_name, blancco_wifi_ssid, blancco_wifi_password) only while none exist. The repo XML carries no site values; the plaintext Wi-Fi password is gone from it. New site_name and site_timezone settings: report and evidence-pack site name, server clock and webapp time zone. No more West Jefferson defaults in webapp/config.py or the dashboard title. Sync-BPRT.ps1 gets the server address filled in at download instead of WJ's GE address. USB on Windows: - scripts/Build-PxeUsb.ps1 runs the same checks as build-usb.sh. It writes one FAT32 CIDATA partition (ISO files plus patched grub.cfg plus payload) with diskpart and reads every file back by SHA-256. - Tested with Windows PowerShell 5.1 in the win11 VM, and the layout installs and first-boots in the pxetest VM. - scripts/stage-usb-share.sh stages the build folder on a share. - site-content/ is optional for both builders. KB docs/kb/How_To_Deploy_PXE_Server_New_Site.html: new-site deployment, Windows only, with a site-vars.yml guide. Checks: webapp tests pass (5 suites); the unattend lint and playbook syntax checks pass; every .ps1 parses; there are no WJ hosts, IPs, names or PC types in playbook/, webapp/, scripts/ or autoinstall/.
507 lines
32 KiB
HTML
507 lines
32 KiB
HTML
{% extends "base.html" %}
|
|
{% import '_ui.html' as ui %}
|
|
{% block title %}Blancco releases - PXE Manager{% endblock %}
|
|
|
|
{% macro mb(n) -%}{{ "%.1f"|format((n or 0) / 1048576) }} MB{%- endmacro %}
|
|
|
|
{% macro prefs(r) -%}{{ r.prefs_version or '-' }}{% if r.vendor_prefs_version and r.vendor_prefs_version != r.prefs_version %} (vendor {{ r.vendor_prefs_version }}){% endif %}{%- endmacro %}
|
|
|
|
{% block extra_head %}
|
|
<style>
|
|
.sec-h { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 8px 16px; padding: 18px 24px 12px; }
|
|
.sec-h .sub { margin-top: 2px; }
|
|
summary.sec-h { flex-wrap: nowrap; align-items: center; }
|
|
.state-line { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; margin-top: 6px; }
|
|
.strip { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; padding: 12px 20px; font-size: 13px; }
|
|
.strip form { margin: 0 0 0 auto; }
|
|
.pulse { width: 10px; height: 10px; border-radius: 5px; background: var(--run); flex: none; animation: pulse 1.4s ease-in-out infinite; }
|
|
@keyframes pulse { 50% { opacity: .35; } }
|
|
@media (prefers-reduced-motion: reduce) { .pulse { animation: none; } }
|
|
.bar { flex: 1 1 160px; height: 6px; border-radius: 3px; background: var(--track-empty); overflow: hidden; min-width: 60px; max-width: 360px; }
|
|
.bar > div { height: 100%; width: 0; background: var(--run); }
|
|
.bar.bad > div, .bar > div.bg-danger { background: var(--fail); }
|
|
.cur { padding: 20px 24px; display: flex; flex-direction: column; gap: 14px; }
|
|
.cur-top { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; }
|
|
.cur-ver { font-size: 24px; font-weight: 600; }
|
|
.facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(170px, 100%), 1fr)); gap: 12px 20px; margin: 0; }
|
|
.facts dt { font-size: 12px; color: var(--muted); font-weight: 400; }
|
|
.facts dd { margin: 2px 0 0; overflow-wrap: anywhere; }
|
|
.sgrid { display: grid; grid-template-columns: 190px minmax(0, 1fr) 190px 14px; align-items: center; gap: 0 16px;
|
|
padding: 12px 240px 12px 24px; font-size: 14px; min-height: 24px; }
|
|
.fgrid { display: grid; grid-template-columns: minmax(0, 1fr) 110px 80px 170px; align-items: center; gap: 0 16px;
|
|
padding: 12px 170px 12px 24px; font-size: 14px; min-height: 36px; }
|
|
.bgrid { display: grid; grid-template-columns: minmax(0, 1fr) 90px 170px 14px; align-items: center; gap: 0 16px;
|
|
padding: 12px 170px 12px 24px; font-size: 14px; min-height: 24px; }
|
|
.rbody { padding: 2px 24px 16px; font-size: 13px; display: flex; flex-direction: column; gap: 8px; line-height: 1.55; }
|
|
.kv { display: grid; grid-template-columns: 130px minmax(0, 1fr); gap: 4px 16px; margin: 0; }
|
|
.kv dt { font-weight: 400; color: var(--muted); }
|
|
.kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
|
.vname { display: flex; align-items: center; gap: 8px; min-width: 0; white-space: nowrap; }
|
|
.upl { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: flex-end; padding: 0 24px 16px; }
|
|
.upl .fld { display: flex; flex-direction: column; gap: 4px; flex: 1 1 280px; min-width: 0; }
|
|
.upl label { font-size: 13px; font-weight: 600; }
|
|
input[type=file].field { padding: 5px 8px; height: auto; min-height: 36px; width: 100%; }
|
|
.prog { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; padding: 0 24px 16px; font-size: 13px; }
|
|
.steps { margin: 0; padding-left: 20px; display: flex; flex-direction: column; gap: 4px; }
|
|
.joblog { margin: 6px 0 0; max-height: 16rem; overflow: auto; padding: 8px 10px; background: var(--surface-2); border: 1px solid var(--line); border-radius: 6px; font-size: 12px; white-space: pre-wrap; color: var(--text); }
|
|
@container (max-width: 900px) {
|
|
.sgrid { grid-template-columns: minmax(0, 1fr) 180px 14px; }
|
|
.sgrid .c-meta { display: none; }
|
|
.fgrid { grid-template-columns: minmax(0, 1fr) 80px; }
|
|
.fgrid .c-folder, .fgrid .c-mod { display: none; }
|
|
.bgrid { grid-template-columns: minmax(0, 1fr) 80px 14px; }
|
|
.bgrid .c-saved { display: none; }
|
|
}
|
|
@container (max-width: 560px) {
|
|
.sgrid .c-meta, .bgrid .c-saved { display: block; }
|
|
.sgrid, .bgrid { grid-template-columns: minmax(0, 1fr) auto 14px; padding: 12px 16px; gap: 4px 10px; }
|
|
.sgrid .c-meta, .bgrid .c-saved { grid-column: 1 / -1; grid-row: 2; }
|
|
.bgrid .c-ver { grid-column: 2; grid-row: 1; }
|
|
.fgrid { grid-template-columns: minmax(0, 1fr) auto; padding: 12px 16px; gap: 2px 10px; }
|
|
.fgrid .c-folder, .fgrid .c-mod { display: none; }
|
|
.ui-row > .act { position: static; padding: 0 16px 10px; display: flex; justify-content: flex-end; }
|
|
.rbody { padding: 2px 16px 14px; }
|
|
.kv { grid-template-columns: minmax(0, 1fr); }
|
|
.kv dd { margin-bottom: 6px; }
|
|
.sec-h, .upl, .prog { padding-left: 16px; padding-right: 16px; }
|
|
.cur { padding: 18px 16px; }
|
|
}
|
|
.site-form { margin-top: 12px; }
|
|
.site-grid { display: grid; grid-template-columns: max-content minmax(0, 28rem); gap: 8px 14px; align-items: center; }
|
|
.site-grid .form-label { margin: 0; }
|
|
.site-act { margin-top: 12px; }
|
|
@media (max-width: 640px) { .site-grid { grid-template-columns: minmax(0, 1fr); } }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<header class="ui-head">
|
|
<div>
|
|
<div class="eyebrow-t">Updates</div>
|
|
<h1>Blancco releases</h1>
|
|
<div class="state-line">
|
|
{{ ui.chip('Live: ' ~ current.version ~ (' (installed by hand)' if current.inferred else ''), 'fail' if current.missing else 'ok') }}
|
|
{% if test %}{{ ui.chip('Test entry: ' ~ (test.version or '?'), 'warn') }}{% endif %}
|
|
{% set n_ready = staged | selectattr('status', 'equalto', 'ready') | list | length %}
|
|
{% if staged %}<span class="sub">{{ staged | length }} staged, {{ n_ready }} ready</span>{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="actions"><a class="ui-btn pri" href="#upload">Upload release</a></div>
|
|
</header>
|
|
|
|
<div class="ui-stack">
|
|
|
|
{# ---- Job ----------------------------------------------------------------- #}
|
|
{% if job.state %}
|
|
<div id="blanccoJob" data-state="{{ job.state }}" class="ui-stack" style="gap: 8px">
|
|
{% if job_running %}
|
|
<div class="panel strip" role="status">
|
|
<span class="pulse" aria-hidden="true"></span>
|
|
<span>Staging {{ job.source or '' }}: <span id="blanccoJobStep">{{ job.step or 'Starting' }}</span></span>
|
|
<span id="blanccoJobMsg" class="sub">{{ job.message or '' }}</span>
|
|
<div class="bar" aria-hidden="true"><div id="blanccoJobBar" style="width: {{ job.percent or 0 }}%"></div></div>
|
|
<form method="post" action="{{ url_for('blancco_update.cancel') }}"
|
|
onsubmit="return confirm('Stop staging and remove the partial files?');">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="ui-btn sm">Cancel</button>
|
|
</form>
|
|
</div>
|
|
{% elif job.state == 'done' %}
|
|
<div class="notice info">Staging {{ job.source or '' }}: {{ job.message }} <span class="sub">({{ job.finished | local_dt }})</span></div>
|
|
{% elif job.state == 'blocked' %}
|
|
<div class="notice"><span><strong>Staging {{ job.source or '' }} stopped: preferences need merging.</strong> {{ job.message }} See the staged release below.</span></div>
|
|
{% elif job.state == 'cancelled' %}
|
|
<div class="notice">Staging {{ job.source or '' }} was cancelled.</div>
|
|
{% else %}
|
|
<div class="errbox">Staging {{ job.source or '' }}: {{ job.message or 'Staging failed.' }}</div>
|
|
{% endif %}
|
|
{% if job.log %}
|
|
<details {% if job_running or job.state in ('failed', 'blocked') %}open{% endif %}>
|
|
<summary class="disc"><span class="chev" aria-hidden="true">▾</span>Job log</summary>
|
|
<pre id="blanccoJobLog" class="mono joblog">{{ job.log | join('\n') }}</pre>
|
|
</details>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# ---- Live release -------------------------------------------------------- #}
|
|
<section class="panel cur" aria-labelledby="live-h">
|
|
<div class="cur-top">
|
|
<div>
|
|
<h2 id="live-h" class="sec" style="font-size: 15px; color: var(--muted)">Live release</h2>
|
|
<div class="cur-ver mono">{{ current.version }}</div>
|
|
</div>
|
|
<span style="margin-left: auto">{% if current.missing %}{{ ui.chip('Files missing', 'fail') }}{% elif current.changed_by_hand %}{{ ui.chip('Changed by hand', 'warn') }}{% else %}{{ ui.chip('Serving', 'ok') }}{% endif %}</span>
|
|
</div>
|
|
{% if current.missing %}<div class="notice bad">Missing from the live folder: <span class="mono">{{ current.missing | join(', ') }}</span></div>{% endif %}
|
|
{% if current.changed_by_hand %}<div class="notice">Files changed outside this page since the last activate or roll back.</div>{% endif %}
|
|
<dl class="facts">
|
|
<div><dt>Blancco kernel</dt><dd class="mono">{{ current.kernel or '-' }}</dd></div>
|
|
<div><dt>Preferences</dt><dd class="mono">{{ prefs(current) }}</dd></div>
|
|
<div><dt>Size</dt><dd>{{ mb(current.size) }}</dd></div>
|
|
<div><dt>Rootfs date</dt><dd class="mono">{{ current.rootfs_mtime | timestamp_fmt if current.rootfs_mtime else '-' }}</dd></div>
|
|
<div><dt>Installed</dt><dd>{% if current.activated %}{{ current.activated | local_dt }}
|
|
{% if current.restored_from %}<div class="sub">rolled back to {{ current.restored_from }}</div>
|
|
{% elif current.source %}<div class="sub">from {{ current.source }}</div>{% endif %}
|
|
{% else %}<span class="muted">by hand (before this page)</span>{% endif %}
|
|
{% if current.inferred %}<div class="sub">version read from the preferences in config.img</div>{% endif %}</dd></div>
|
|
</dl>
|
|
<div class="sub" style="overflow-wrap: anywhere">Served from <span class="mono">{{ current.path }}</span>.</div>
|
|
</section>
|
|
|
|
{# ---- Site settings ------------------------------------------------------- #}
|
|
<section class="panel cur" aria-labelledby="site-h">
|
|
<div class="cur-top">
|
|
<div>
|
|
<h2 id="site-h" class="sec" style="font-size: 15px; color: var(--muted)">Site settings</h2>
|
|
<div class="sub">On every erasure report, and the Wi-Fi Blancco joins to license erasures in the GE Blancco cloud. Saved into every release, including ones staged later.</div>
|
|
</div>
|
|
<span style="margin-left: auto">{% if not site.configured %}{{ ui.chip('Not set', 'warn') }}{% elif not site.business_location or not site.wifi_password_set %}{{ ui.chip('Incomplete', 'warn') }}{% else %}{{ ui.chip('Set', 'ok') }}{% endif %}</span>
|
|
</div>
|
|
<form method="post" action="{{ url_for('blancco_update.site_settings') }}" class="site-form" autocomplete="off">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<div class="site-grid">
|
|
<label class="form-label" for="bpLoc">Business location</label>
|
|
<input type="text" class="form-control" id="bpLoc" name="business_location" maxlength="128"
|
|
value="{{ site.business_location }}" placeholder="Site name, e.g. Lynn">
|
|
<label class="form-label" for="bpSsid">Wi-Fi network (SSID)</label>
|
|
<input type="text" class="form-control" id="bpSsid" name="wifi_ssid" maxlength="128" required
|
|
value="{{ site.wifi_ssid }}">
|
|
<label class="form-label" for="bpPass">Wi-Fi password</label>
|
|
<input type="password" class="form-control" id="bpPass" name="wifi_password" maxlength="128"
|
|
autocomplete="new-password"
|
|
placeholder="{{ 'Set (leave empty to keep it)' if site.wifi_password_set else 'Not set' }}">
|
|
</div>
|
|
<div class="site-act"><button type="submit" class="ui-btn pri sm">Save</button></div>
|
|
</form>
|
|
</section>
|
|
|
|
{# ---- Staged -------------------------------------------------------------- #}
|
|
<section class="panel ui-list" aria-labelledby="stg-h">
|
|
<div class="sec-h"><div><h2 id="stg-h" class="sec">Staged releases</h2>
|
|
<div class="sub">Next step for each: add a test entry, test-boot it from the PXE menu, then activate.</div></div></div>
|
|
{% for s in staged %}
|
|
{% set q_test = ('Add a test entry for ' ~ s.name ~ ' to the PXE boot menu' ~ (' (replacing the one for ' ~ (test.version or '?') ~ ')' if test else '') ~ '? The menu is backed up first.') %}
|
|
{% set q_act = ('Make Blancco ' ~ s.name ~ ' the live release? Every Blancco boot from now on gets it. The current files (' ~ current.version ~ ') are backed up first and can be rolled back.') %}
|
|
<div class="ui-row">
|
|
<details {% if s.status in ('blocked', 'failed') %}open{% endif %}>
|
|
<summary class="sgrid">
|
|
<span class="vname"><span class="mono" style="font-weight: 600">{{ s.name }}</span>{% if s.in_menu %}{{ ui.chip('in boot menu', 'warn') }}{% endif %}</span>
|
|
<span class="tip c-meta" data-tip="kernel {{ s.kernel or '-' }}, preferences {{ prefs(s) }}, {{ mb(s.size) }}"><span class="trunc sub">Staged {{ s.staged | local_dt }}{% if s.source %} from {{ s.source }}{% endif %}</span></span>
|
|
<span>
|
|
{% if s.status == 'ready' and s.in_menu %}{{ ui.chip('Ready: testing', 'ok') }}
|
|
{% elif s.status == 'ready' %}{{ ui.chip('Ready to test', 'ok') }}
|
|
{% elif s.status == 'blocked' %}{{ ui.chip('Needs preferences merge', 'act') }}
|
|
{% elif s.status == 'failed' %}{{ ui.chip('Failed', 'err') }}
|
|
{% else %}{{ ui.chip(s.status, 'neutral') }}{% endif %}
|
|
</span>
|
|
<span class="chev" aria-hidden="true">▾</span>
|
|
</summary>
|
|
<div class="rbody">
|
|
{% if s.status == 'failed' and s.error %}<div class="errbox">{{ s.error }}</div>{% endif %}
|
|
<dl class="kv">
|
|
<dt>Blancco kernel</dt><dd class="mono">{{ s.kernel or '-' }}</dd>
|
|
<dt>Preferences</dt><dd class="mono">{{ prefs(s) }}{% if s.merged_prefs %} <span class="sub">(merged preferences)</span>{% endif %}</dd>
|
|
<dt>Size</dt><dd>{{ mb(s.size) }}</dd>
|
|
<dt>Staged</dt><dd>{{ s.staged | local_dt }}{% if s.source %} from <span class="mono">{{ s.source }}</span>{% endif %}</dd>
|
|
</dl>
|
|
{% if s.status == 'ready' %}
|
|
<div class="sub">{% if s.in_menu %}The test entry boots this release. When a test boot works, activate it.{% else %}Add a test entry, test-boot it, then activate.{% endif %}</div>
|
|
{% endif %}
|
|
{% if s.status == 'blocked' %}
|
|
<div class="notice">{{ s.blocked }}</div>
|
|
<ol class="steps">
|
|
<li><a href="{{ url_for('blancco_update.vendor_prefs', version=s.name) }}">Download the vendor preferences.xml of {{ s.name }}</a>.</li>
|
|
<li>Carry our settings across from <span class="mono">playbook/blancco-preferences.xml</span> (report share
|
|
<span class="mono">{{ server }}</span>/<span class="mono">blancco-reports</span>, <span class="mono">auto_backup</span>, WiFi,
|
|
screensaver, business location and the rest), keeping the new file's <span class="mono"><version></span> block and any new settings.</li>
|
|
<li>Upload the merged file here; staging then finishes. Commit it to the repo as <span class="mono">playbook/blancco-preferences.xml</span>.</li>
|
|
</ol>
|
|
<form method="post" action="{{ url_for('blancco_update.merged_prefs', version=s.name) }}" enctype="multipart/form-data"
|
|
style="display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; margin: 0">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<div style="display: flex; flex-direction: column; gap: 4px; flex: 1 1 260px; min-width: 0">
|
|
<label for="prefs-{{ loop.index }}" style="font-weight: 600">Merged preferences.xml</label>
|
|
<input type="file" class="field" id="prefs-{{ loop.index }}" name="prefs_file" accept=".xml" required>
|
|
</div>
|
|
<button type="submit" class="ui-btn pri">Upload and finish staging</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
</details>
|
|
<div class="act">
|
|
{% if s.status == 'ready' and not s.in_menu %}
|
|
<form method="post" action="{{ url_for('blancco_update.test_add', version=s.name) }}" style="margin: 0"
|
|
onsubmit="return confirm({{ q_test | tojson | forceescape }});">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="ui-btn pri">Add test entry</button>
|
|
</form>
|
|
{% elif s.status == 'ready' %}
|
|
<form method="post" action="{{ url_for('blancco_update.activate', version=s.name) }}" style="margin: 0"
|
|
onsubmit="return confirm({{ q_act | tojson | forceescape }});">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="ui-btn pri">Activate</button>
|
|
</form>
|
|
{% endif %}
|
|
{% call ui.more('staged release ' ~ s.name) %}
|
|
{% if s.status == 'ready' and not s.in_menu %}
|
|
<form method="post" action="{{ url_for('blancco_update.activate', version=s.name) }}"
|
|
onsubmit="return confirm({{ q_act | tojson | forceescape }});">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit">Activate without a test boot</button>
|
|
</form>
|
|
<div class="sep"></div>
|
|
{% endif %}
|
|
<form method="post" action="{{ url_for('blancco_update.delete_staged', version=s.name) }}"
|
|
onsubmit="return confirm({{ ('Delete staged ' ~ s.name ~ '?') | tojson | forceescape }});">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="del">Delete staged release</button>
|
|
</form>
|
|
{% endcall %}
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="hidden-bar">Nothing staged. Upload a release file below, then Stage it.</div>
|
|
{% endfor %}
|
|
</section>
|
|
|
|
{# ---- Test entry ---------------------------------------------------------- #}
|
|
<section class="panel" aria-labelledby="test-h" style="padding: 18px 24px; display: flex; flex-direction: column; gap: 10px">
|
|
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px">
|
|
<div style="flex: 1 1 300px; min-width: 0">
|
|
<h2 id="test-h" class="sec" style="font-size: 15px">Test entry in the PXE boot menu</h2>
|
|
{% if test %}
|
|
<div class="sub" style="overflow-wrap: anywhere">The menu has <span class="mono">{{ test.item or 'a blancco_staged entry' }}</span>, booting the staged
|
|
<strong class="mono">{{ test.version or '?' }}</strong> from <span class="mono">/blancco/staged/{{ test.version }}/</span>.</div>
|
|
{% else %}
|
|
<div class="sub">No test entry. Add test entry on a staged release puts one under the normal Blancco entry.</div>
|
|
{% endif %}
|
|
</div>
|
|
{% if test %}
|
|
<form method="post" action="{{ url_for('blancco_update.test_remove') }}" style="margin: 0"
|
|
onsubmit="return confirm('Remove the test entry from the PXE boot menu? The menu is backed up first.');">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="ui-btn">Remove test entry</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
{% if test and test.efi is false %}
|
|
<div class="notice">The entry's UEFI branch is off: when it was added, the UEFI initrd did not read
|
|
<span class="mono">blancco.base=</span>, so a UEFI test boot would have run the LIVE release. A UEFI boot of the entry
|
|
says so and returns to the menu. After rebuilding the initrd, add the test entry again.</div>
|
|
{% endif %}
|
|
{% if initrd.state == 'no' %}
|
|
<div class="notice bad"><span>The UEFI initrd <span class="mono">{{ initrd.file or 'kexec-initrd' }}</span> predates <span class="mono">blancco.base=</span>: booted, it would run the LIVE release, so a test
|
|
entry added now has its UEFI branch off (legacy BIOS still works). Rebuild the initrd with the current
|
|
<span class="mono">playbook/blancco-init.sh</span>, then add the entry again (the boot screen shows "release under test" when it works).</span></div>
|
|
{% endif %}
|
|
<details>
|
|
<summary class="disc"><span class="chev" aria-hidden="true">▾</span>How the test entry boots
|
|
{% if initrd.state == 'yes' %}{{ ui.chip('UEFI supported', 'ok') }}{% elif initrd.state == 'no' %}{{ ui.chip('UEFI not supported', 'err') }}{% else %}{{ ui.chip('UEFI unknown', 'off') }}{% endif %}</summary>
|
|
<ul class="sub" style="margin: 4px 0 0; padding-left: 18px; line-height: 1.6; display: flex; flex-direction: column; gap: 4px">
|
|
<li><strong>Legacy BIOS:</strong> the entry boots Blancco's own kernel from the staged folder
|
|
(<span class="mono">archiso_http_srv</span> pointed at it). Works with any initrd.</li>
|
|
<li><strong>UEFI:</strong> the entry copies the live <span class="mono">:blancco_efi</span> kernel line (Ubuntu kernel,
|
|
Secure Boot as the normal entry) and adds <span class="mono">blancco.base=/blancco/staged/<version></span>, which
|
|
the init in <span class="mono">{{ initrd.file or 'kexec-initrd' }}</span> must understand:
|
|
{% if initrd.state == 'yes' %}supported.{% elif initrd.state == 'no' %}not supported (see above).{% else %}unknown, {{ initrd.detail }}.{% endif %}</li>
|
|
<li>Add, replace and remove edit only the test entry's lines; each edit saves
|
|
<span class="mono">GetPxeScript.aspx.bak-<time>-pre-blancco-staged*</span> next to the menu first. Activating the
|
|
version under test removes its entry.</li>
|
|
</ul>
|
|
</details>
|
|
</section>
|
|
|
|
{# ---- Release files and upload -------------------------------------------- #}
|
|
<section class="panel ui-list" id="upload" aria-labelledby="up-h">
|
|
<div class="sec-h"><div><h2 id="up-h" class="sec">Release files</h2>
|
|
<div class="sub">Upload the vendor ISO or USB image (about 1 GB), then Stage it.</div></div></div>
|
|
<form id="blanccoUpload" method="post" action="{{ url_for('blancco_update.upload') }}" enctype="multipart/form-data" class="upl">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<div class="fld">
|
|
<label for="blanccoFile">Vendor ISO or USB image</label>
|
|
<input type="file" class="field" id="blanccoFile" name="release_file" accept=".iso,.img,.gz" required aria-describedby="blanccoFileHelp">
|
|
</div>
|
|
<button type="submit" class="ui-btn pri">Upload</button>
|
|
<span id="blanccoFileHelp" class="sub" style="flex-basis: 100%; overflow-wrap: anywhere">Streamed to <span class="mono">{{ upload_dir }}</span>.</span>
|
|
</form>
|
|
<div id="blanccoUploadProgress" class="prog" style="display: none;" role="status">
|
|
<span id="blanccoUploadText">Uploading...</span>
|
|
<div class="bar"><div id="blanccoUploadBar" style="width: 0%;"></div></div>
|
|
</div>
|
|
{% for s in sources %}
|
|
<div class="ui-row">
|
|
<div class="fgrid">
|
|
<span class="tip" data-tip="{{ s.name }}"><span class="trunc mono">{{ s.name }}</span></span>
|
|
<span class="sub c-folder">{{ s.key }}</span>
|
|
<span class="sub">{{ mb(s.size) }}</span>
|
|
<span class="sub mono c-mod">{{ s.modified | timestamp_fmt }}</span>
|
|
</div>
|
|
<div class="act" style="top: 6px">
|
|
<form method="post" action="{{ url_for('blancco_update.stage') }}" style="margin: 0">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="source" value="{{ s.key }}:{{ s.name }}">
|
|
<button type="submit" class="ui-btn" {% if job_running %}disabled{% endif %}>Stage</button>
|
|
</form>
|
|
{% if s.deletable %}
|
|
{% call ui.more(s.name) %}
|
|
<form method="post" action="{{ url_for('blancco_update.delete_upload', name=s.name) }}"
|
|
onsubmit="return confirm({{ ('Delete ' ~ s.name ~ '?') | tojson | forceescape }});">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="del">Delete file</button>
|
|
</form>
|
|
{% endcall %}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="hidden-bar">No release files yet. Upload the vendor ISO or USB image above.</div>
|
|
{% endfor %}
|
|
<div class="hidden-bar" style="overflow-wrap: anywhere"><span>Files copied in by hand are listed too, from
|
|
{% for key, folder in source_dirs %}<span class="mono">{{ folder }}</span>{{ ', ' if not loop.last }}{% endfor %}
|
|
(<span class="mono">.iso</span>, <span class="mono">.img</span>, <span class="mono">.img.gz</span>, <span class="mono">.iso.gz</span>).</span></div>
|
|
</section>
|
|
|
|
{# ---- Backups (folded) ----------------------------------------------------- #}
|
|
<section class="panel ui-list" aria-labelledby="bk-h">
|
|
<details>
|
|
<summary class="sec-h">
|
|
<div><h2 id="bk-h" class="sec" style="font-size: 15px">Backups</h2>
|
|
<div class="sub">Activating a release saves the one it replaces here. A rollback first saves what is live.</div></div>
|
|
<span style="display: flex; align-items: center; gap: 10px">{{ ui.chip(backups | length ~ ' backup' ~ ('s' if backups | length != 1), 'neutral') }}<span class="chev" aria-hidden="true">▾</span></span>
|
|
</summary>
|
|
{% for b in backups %}
|
|
<div class="ui-row">
|
|
<details>
|
|
<summary class="bgrid">
|
|
<span class="tip" data-tip="{{ b.name }}"><span class="trunc mono">{{ b.name }}</span></span>
|
|
<span class="mono c-ver">{{ b.version }}</span>
|
|
<span class="sub mono c-saved">{{ b.created | local_dt if b.created is string else b.created | timestamp_fmt }}</span>
|
|
<span class="chev" aria-hidden="true">▾</span>
|
|
</summary>
|
|
<div class="rbody">
|
|
{% if b.reason %}<div>{{ b.reason }}</div>{% endif %}
|
|
{% if not b.complete %}<div class="notice bad">Incomplete, cannot be rolled back to: missing <span class="mono">{{ b.missing | join(', ') }}</span></div>{% endif %}
|
|
<dl class="kv">
|
|
<dt>Blancco kernel</dt><dd class="mono">{{ b.kernel or '-' }}</dd>
|
|
<dt>Preferences</dt><dd class="mono">{{ prefs(b) }}</dd>
|
|
<dt>Size</dt><dd>{{ mb(b.size) }}</dd>
|
|
</dl>
|
|
</div>
|
|
</details>
|
|
<div class="act">
|
|
{% if b.complete %}
|
|
<form method="post" action="{{ url_for('blancco_update.rollback', name=b.name) }}" style="margin: 0"
|
|
onsubmit="return confirm({{ ('Put ' ~ b.name ~ ' back live? What is live now (' ~ current.version ~ ') is backed up first.') | tojson | forceescape }});">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="ui-btn">Roll back</button>
|
|
</form>
|
|
{% else %}{{ ui.chip('Incomplete', 'off') }}{% endif %}
|
|
{% call ui.more(b.name) %}
|
|
<form method="post" action="{{ url_for('blancco_update.delete_backup', name=b.name) }}"
|
|
onsubmit="return confirm({{ ('Delete ' ~ b.name ~ '? It cannot be rolled back to afterwards.') | tojson | forceescape }});">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="del">Delete backup</button>
|
|
</form>
|
|
{% endcall %}
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="hidden-bar">No backups yet. Activating a release saves the one it replaces here.</div>
|
|
{% endfor %}
|
|
<div class="hidden-bar" style="overflow-wrap: anywhere"><span>Folders <span class="mono">{{ current.path }}/backup-*</span>, about 1 GB each. Taking one is instant
|
|
(hard links of the live files before they are replaced); the restored backup's folder is released after a rollback, because its files are the live files.</span></div>
|
|
</details>
|
|
</section>
|
|
|
|
{# ---- How it works -------------------------------------------------------- #}
|
|
<details>
|
|
<summary class="disc"><span class="chev" aria-hidden="true">▾</span>What a release changes</summary>
|
|
<div class="sub" style="max-width: 900px; line-height: 1.6; display: flex; flex-direction: column; gap: 8px; margin-top: 4px; overflow-wrap: anywhere">
|
|
<p style="margin: 0">Installs a new Blancco Drive Eraser release from the vendor ISO (or the vendor's USB image, <span class="mono">.img</span>
|
|
or <span class="mono">.img.gz</span>). Staging extracts the release next to the live one, applies the same changes as
|
|
<span class="mono">scripts/prepare-boot-tools.sh</span> (our preferences written into <span class="mono">config.img</span>,
|
|
the initramfs network patch), and a test entry in the PXE boot menu boots the staged copy. Only Activate
|
|
changes what the normal Blancco entry boots, and the files it replaces are kept as a backup you can roll back to.</p>
|
|
<p style="margin: 0">Replaced in <span class="mono">{{ current.path }}</span>: <span class="mono">arch/x86_64/airootfs.sfs</span>,
|
|
<span class="mono">vmlinuz-bde-linux</span>, <span class="mono">initramfs-bde-linux.img</span> (copytoram network patch),
|
|
<span class="mono">intel-ucode.img</span>, <span class="mono">amd-ucode.img</span>, <span class="mono">config.img</span>
|
|
(vendor cpio with our preferences written in at the same size), <span class="mono">preferences.xml</span>,
|
|
<span class="mono">config-clean.xml</span> (the licence container from <span class="mono">config.img</span>) and, when the
|
|
ISO has one, <span class="mono">blancco-shim.efi</span>. Never touched: <span class="mono">vmlinuz-ubuntu*</span>,
|
|
<span class="mono">kexec-initrd*</span>, <span class="mono">kmod*.tar.gz</span>, <span class="mono">grubx64.efi</span>,
|
|
<span class="mono">shimx64.efi</span>, <span class="mono">blancco-progress.py</span>.</p>
|
|
<p style="margin: 0">Our preferences come from the live <span class="mono">preferences.xml</span> (the playbook deploys
|
|
<span class="mono">playbook/blancco-preferences.xml</span> there). They are a merge onto one vendor release's file,
|
|
so when a release's <span class="mono"><version></span> differs, staging stops until a merged file is uploaded.
|
|
After activating such a release, commit the merged file to the repo, or the next playbook run puts the old one back.</p>
|
|
<p style="margin: 0">A playbook run also copies <span class="mono">boot-tools/blancco/*</span> from the USB over the live files. After activating
|
|
a release here, refresh that copy (<span class="mono">scripts/prepare-boot-tools.sh</span> with the new ISO), or a playbook run
|
|
puts the old release back.</p>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
(function () {
|
|
var csrf = document.querySelector('meta[name="csrf-token"]').content;
|
|
|
|
// Upload: stream the raw file (no multipart parse, no copy in /tmp) with a progress bar.
|
|
var form = document.getElementById('blanccoUpload');
|
|
form.addEventListener('submit', function (ev) {
|
|
var input = document.getElementById('blanccoFile');
|
|
if (!input.files.length || !window.XMLHttpRequest) { return; }
|
|
ev.preventDefault();
|
|
var file = input.files[0];
|
|
var box = document.getElementById('blanccoUploadProgress');
|
|
var bar = document.getElementById('blanccoUploadBar');
|
|
var text = document.getElementById('blanccoUploadText');
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open('POST', form.action + '?name=' + encodeURIComponent(file.name));
|
|
xhr.setRequestHeader('X-CSRF-Token', csrf);
|
|
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
|
|
xhr.upload.onprogress = function (e) {
|
|
if (!e.lengthComputable) { return; }
|
|
var pct = 100 * e.loaded / e.total;
|
|
bar.style.width = pct.toFixed(1) + '%';
|
|
text.textContent = 'Uploading ' + file.name + ': ' + Math.round(e.loaded / 1048576) + ' of ' +
|
|
Math.round(e.total / 1048576) + ' MB';
|
|
};
|
|
xhr.onload = function () {
|
|
var r = {};
|
|
try { r = JSON.parse(xhr.responseText); } catch (e) {}
|
|
if (xhr.status === 200 && r.ok) { location.reload(); return; }
|
|
text.textContent = 'Upload failed: ' + (r.error || ('HTTP ' + xhr.status));
|
|
bar.classList.add('bg-danger');
|
|
};
|
|
xhr.onerror = function () { text.textContent = 'Upload failed: connection lost.'; bar.classList.add('bg-danger'); };
|
|
box.style.display = '';
|
|
form.querySelector('button[type=submit]').disabled = true;
|
|
xhr.send(file);
|
|
});
|
|
|
|
// Follow a running staging job; reload when it ends.
|
|
var job = document.getElementById('blanccoJob');
|
|
if (!job || job.dataset.state !== 'running') { return; }
|
|
var URL = '{{ url_for("blancco_update.status") }}';
|
|
function tick() {
|
|
fetch(URL, { credentials: 'same-origin' })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (d) {
|
|
if (d.state !== 'running') { location.reload(); return; }
|
|
document.getElementById('blanccoJobStep').textContent = d.step || '';
|
|
document.getElementById('blanccoJobMsg').textContent = d.message || '';
|
|
document.getElementById('blanccoJobBar').style.width = (d.percent || 0) + '%';
|
|
var log = document.getElementById('blanccoJobLog');
|
|
if (log && d.log) { log.textContent = d.log.join('\n'); }
|
|
setTimeout(tick, 2000);
|
|
})
|
|
.catch(function () { setTimeout(tick, 5000); });
|
|
}
|
|
setTimeout(tick, 1500);
|
|
})();
|
|
</script>
|
|
{% endblock %}
|