Updates group: - Dell drivers and BIOS: check Dell's catalogs against all three driver maps and models.txt, download with a shared speed cap, repack, preview, apply with backups and rollback. - iPXE boot files: check GitHub for the signed Secure Boot set, verify the sha256 and each file's signer, install atomically, roll back. Includes the by-hand procedure. - Blancco: upload a release, stage it, test-boot it from the menu, activate, roll back. - BPRT sync: validate a batch uploaded from the MediaCreator Lite cache (scripts/windows/Sync-BPRT.ps1 or a zip), show the diff, swap with the site overlay files kept. Images: one sidebar entry per image with Overview, Drivers, Unattend and OS and Packages tabs. Every save shows a diff first, keeps the file's format, makes a backup and can be undone. Driver lint runs inline. Enrollment: upload standard/engineer MCL packages, one per tenant/purpose, with archive and restore. Hardware coverage counts engineer as Win11.
400 lines
18 KiB
HTML
400 lines
18 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}{{ friendly_name }} - OS and Packages{% endblock %}
|
|
|
|
{% block extra_head %}
|
|
<style>
|
|
.osp-table td { vertical-align: top; white-space: normal; }
|
|
.osp-table td.num { width: 40px; color: var(--text-light); font-weight: 600; text-align: center; }
|
|
.osp-table .wrap { overflow-wrap: anywhere; }
|
|
.osp-table .sub { font-size: 12px; color: var(--text-light); }
|
|
.osp-table .form-control-sm { min-width: 10rem; }
|
|
.osp-table tr.row-new td { background: rgba(65, 129, 255, 0.06); }
|
|
#ospModelTable td { vertical-align: middle; }
|
|
.osp-note { font-size: 13px; color: var(--text-light); padding: 0.75rem 1.25rem; border-top: 1px solid var(--border); }
|
|
.osp-explain { font-size: 13.5px; }
|
|
.osp-explain p:last-child { margin-bottom: 0; }
|
|
.dirty-flag { font-size: 12px; color: var(--warning-dark); display: none; align-items: center; gap: 0.35rem; }
|
|
[data-theme="dark"] .dirty-flag { color: var(--warning); }
|
|
.dirty .dirty-flag { display: inline-flex; }
|
|
.empty-row td { text-align: center; color: var(--text-light); padding: 1.25rem; }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
{% set header_actions %}
|
|
<span class="dirty-flag" id="ospDirty"><i class="bi bi-circle-fill" style="font-size:8px;"></i> Unsaved changes</span>
|
|
{% if last_save %}
|
|
<form method="POST" action="{{ url_for('images.os_undo', image_type=image_type) }}" class="d-inline"
|
|
onsubmit="return confirm('Put back {{ last_save.files | map(attribute='name') | join(', ') }} as they were before the save of {{ last_save.ts | local_dt }}?');">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="btn btn-outline-secondary btn-sm" title="Last save: {{ last_save.ts | local_dt }}">
|
|
<i class="bi bi-arrow-counterclockwise"></i> Undo last save
|
|
</button>
|
|
</form>
|
|
{% endif %}
|
|
<button type="button" class="btn btn-success btn-sm" id="ospReview" disabled>
|
|
<i class="bi bi-file-earmark-diff"></i> Review and save
|
|
</button>
|
|
{% endset %}
|
|
{% include "_image_header.html" %}
|
|
|
|
<p class="text-muted osp-explain">
|
|
What PESetup puts on the disk besides drivers: the Windows image from
|
|
<span class="mono">OperatingSystem.json</span>, then the offline servicing packages from
|
|
<span class="mono">packages.json</span> (both in <span class="mono">Deploy/Control</span>).
|
|
Edits here are staged until <strong>Review and save</strong>, which shows the exact diff of every file first.
|
|
</p>
|
|
|
|
{# ==================== Operating system ==================== #}
|
|
<div class="card" data-section="operating_systems">
|
|
<div class="card-header">
|
|
<span class="d-flex align-items-center gap-2">
|
|
<i class="bi bi-windows"></i> Operating system
|
|
<span class="badge badge-secondary">{{ oses | length }}</span>
|
|
</span>
|
|
<span class="small text-muted fw-normal mono">OperatingSystem.json</span>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="table mb-0 osp-table" id="ospOsTable">
|
|
<thead>
|
|
<tr>
|
|
<th>#</th>
|
|
<th>Windows</th>
|
|
<th>osId</th>
|
|
<th>Status</th>
|
|
<th>install.wim</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for r in oses %}
|
|
{% set osv = r.osv %}
|
|
<tr data-json='{{ r.entry | tojson_ordered }}'>
|
|
<td class="num order-num">{{ loop.index }}</td>
|
|
<td>
|
|
<strong>{{ osv.productName or osv.marketingName or 'Windows' }}</strong>
|
|
<div class="sub">{{ osv.marketingName }}{% if osv.versionNumber %} | version {{ osv.versionNumber }}{% endif %}{% if osv.buildNumber %} | build {{ osv.buildNumber }}{% endif %}</div>
|
|
</td>
|
|
<td class="mono">{{ osv.id }}</td>
|
|
<td class="text-nowrap">
|
|
{% if osv.isActive %}<span class="badge badge-success">Active</span>{% else %}<span class="badge badge-secondary">Inactive</span>{% endif %}
|
|
{% if os_selection is not none and osv.id | string == os_selection | string %}<span class="badge badge-primary" title="user_selections.json OperatingSystemSelection">Selected</span>{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="mono wrap">{{ r.dest or '(no DestinationDir)' }}</div>
|
|
<div class="sub">
|
|
{% if r.wim.exists %}<span class="badge badge-success">On disk</span> {{ r.wim.size | filesizeformat }}, modified {{ r.wim.mtime | local_dt }}
|
|
{% else %}<span class="badge badge-danger">install.wim not on disk</span>{% endif %}
|
|
| index {{ osv.wimindex if osv.wimindex is defined else (r.entry.wimindex if r.entry.wimindex is defined else '-') }}
|
|
{% if osv.wim and osv.wim.size and r.wim.exists and osv.wim.size != r.wim.size %}
|
|
<span class="badge badge-warning" title="size in OperatingSystem.json: {{ osv.wim.size }}">size differs from the JSON</span>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
<td class="text-end">
|
|
<button type="button" class="btn btn-outline-danger btn-row-action remove-row" title="Remove this entry">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
<tr class="empty-row" {% if oses %}style="display:none"{% endif %}><td colspan="6">No operating system entries. PESetup has nothing to apply.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="osp-note">
|
|
PESetup applies the image index of <span class="mono">install.wim</span> from the entry's folder
|
|
(<span class="mono">*destinationdir*</span> is this image's share).
|
|
{% if os_selection is not none %}The entry marked Selected is the one the media was built for
|
|
(<span class="mono">OperatingSystemSelection = {{ os_selection }}</span>).{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{# ==================== Packages ==================== #}
|
|
<div class="card" data-section="packages">
|
|
<div class="card-header">
|
|
<span class="d-flex align-items-center gap-2">
|
|
<i class="bi bi-box-seam"></i> Packages
|
|
<span class="badge badge-secondary">{{ packages | length }}</span>
|
|
</span>
|
|
<div class="header-actions">
|
|
<span class="small text-muted fw-normal mono align-self-center">packages.json</span>
|
|
<button type="button" class="btn btn-sm btn-outline-primary"
|
|
data-bs-toggle="modal" data-bs-target="#packageUploadModal">
|
|
<i class="bi bi-upload"></i> Upload file
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="table mb-0 osp-table" id="ospPkgTable">
|
|
<thead>
|
|
<tr>
|
|
<th>#</th>
|
|
<th>Package</th>
|
|
<th>File</th>
|
|
<th>osId</th>
|
|
<th>Enabled</th>
|
|
<th>On disk</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for r in packages %}
|
|
{% set p = r.entry %}
|
|
<tr data-json='{{ p | tojson_ordered }}'>
|
|
<td class="num order-num">{{ loop.index }}</td>
|
|
<td>
|
|
<strong>{{ p.name or '(no name)' }}</strong>
|
|
{% if p.comment %}<div class="sub">{{ p.comment }}</div>{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="mono wrap">{{ p.fileName or p.FileName or '' }}</div>
|
|
<div class="sub mono wrap">{{ p.destinationDir or p.DestinationDir or '' }}</div>
|
|
</td>
|
|
<td class="mono">{{ p.osId }}</td>
|
|
<td>
|
|
<div class="form-check form-switch m-0">
|
|
<input class="form-check-input pkg-enabled" type="checkbox" role="switch"
|
|
aria-label="Enabled" {% if p.enabled %}checked{% endif %}>
|
|
</div>
|
|
</td>
|
|
<td class="text-nowrap">
|
|
{% if r.file.exists %}<span class="badge badge-success">Yes</span><div class="sub">{{ r.file.size | filesizeformat }}</div>
|
|
{% else %}<span class="badge badge-danger">No</span>{% endif %}
|
|
</td>
|
|
<td class="text-end">
|
|
<button type="button" class="btn btn-outline-danger btn-row-action remove-row" title="Remove this entry">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
<tr class="empty-row" {% if packages %}style="display:none"{% endif %}><td colspan="7">No packages configured.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="osp-note">
|
|
A package is applied when it is enabled and its osId list contains the OS being installed.
|
|
Removing an entry here leaves the file on disk.
|
|
</div>
|
|
</div>
|
|
|
|
{# ==================== Hardware models (user_selections.json) ==================== #}
|
|
<div class="card" data-section="hardware_models">
|
|
<div class="card-header">
|
|
<span class="d-flex align-items-center gap-2">
|
|
<i class="bi bi-cpu"></i> Hardware Models
|
|
<span class="badge badge-secondary">{{ models | length }}</span>
|
|
</span>
|
|
<div class="header-actions">
|
|
<span class="small text-muted fw-normal mono align-self-center">Tools/user_selections.json</span>
|
|
<button type="button" class="btn btn-sm btn-outline-primary" id="ospAddModel" {% if not has_selections %}disabled{% endif %}>
|
|
<i class="bi bi-plus-lg"></i> Add
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body osp-explain border-bottom">
|
|
<p class="mb-2">
|
|
<strong>Media build selection, not used while imaging.</strong>
|
|
Media Creator Lite saved this list when the media was built: the driver families (Family ID) whose packs it
|
|
downloaded, and the OS it built for. <span class="mono">scripts/Download-Drivers.ps1</span> and
|
|
<span class="mono">Upload-Image.ps1</span> read it to choose which driver packs and OS to fetch and upload.
|
|
</p>
|
|
<p class="text-muted">
|
|
PESetup never reads this file: at imaging it matches the PC's WMI model against
|
|
<a href="{{ image_tab_url(image_type, 'drivers') }}">HardwareDriver.json</a> alone, so adding a model here does not
|
|
give a PC drivers. The Driver entries column counts map entries with the same <span class="mono">family</span>.
|
|
</p>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="table mb-0 osp-table" id="ospModelTable">
|
|
<thead>
|
|
<tr>
|
|
<th>#</th>
|
|
<th>Model</th>
|
|
<th>Family ID</th>
|
|
<th>Driver entries</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for r in models %}
|
|
<tr data-json='{{ r.m | tojson_ordered }}'>
|
|
<td class="num order-num">{{ loop.index }}</td>
|
|
<td><input type="text" class="form-control form-control-sm" data-field="Model" value="{{ r.m.Model }}" aria-label="Model"></td>
|
|
<td><input type="text" class="form-control form-control-sm" data-field="Id" value="{{ r.m.Id }}" aria-label="Family ID"></td>
|
|
<td class="text-nowrap">
|
|
{% if r.fam.entries %}
|
|
<span class="badge {{ 'badge-success' if r.fam.on_disk == r.fam.entries else 'badge-warning' }}">{{ r.fam.entries }} entr{{ 'ies' if r.fam.entries != 1 else 'y' }}</span>
|
|
<div class="sub">{{ r.fam.on_disk }} zip{{ 's' if r.fam.on_disk != 1 }} on disk</div>
|
|
{% else %}
|
|
<span class="badge badge-secondary">None</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="text-end">
|
|
<button type="button" class="btn btn-outline-danger btn-row-action remove-row" title="Remove this model">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
<tr class="empty-row" {% if models %}style="display:none"{% endif %}><td colspan="5">{% if has_selections %}No hardware models selected.{% else %}No user_selections.json for this image.{% endif %}</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Package upload modal (unchanged from the old Configuration page) -->
|
|
<div class="modal fade" id="packageUploadModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<form method="POST" action="{{ url_for('images_packages_upload', image_type=image_type) }}" enctype="multipart/form-data">
|
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">Upload package</h3>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="form-field">
|
|
<label class="form-label">Package file</label>
|
|
<input type="file" class="form-control" name="package_file" required>
|
|
<div class="form-text">Lands in <code>{{ image_type }}/Deploy/Packages/</code>.</div>
|
|
</div>
|
|
<div class="form-field">
|
|
<label class="form-label">Destination directory (optional)</label>
|
|
<input type="text" class="form-control mono" name="destination_dir"
|
|
placeholder="*destinationdir*\Packages">
|
|
<div class="form-text">Setting this also appends an entry to <code>packages.json</code>.</div>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="overwrite" value="1" id="pkgOverwriteCheck">
|
|
<label class="form-check-label" for="pkgOverwriteCheck">Overwrite if same filename exists</label>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Upload</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
// OS and Packages: stage edits in the tables, then Review and save posts
|
|
// only the sections that changed (views/images.py os_preview / os_save).
|
|
(function () {
|
|
var dirty = {};
|
|
var reviewBtn = document.getElementById('ospReview');
|
|
var head = document.querySelector('.image-head');
|
|
|
|
function markDirty(section) {
|
|
dirty[section] = true;
|
|
reviewBtn.disabled = false;
|
|
if (head) head.classList.add('dirty');
|
|
}
|
|
|
|
function rowsOf(tableId) {
|
|
return Array.prototype.slice.call(
|
|
document.querySelectorAll('#' + tableId + ' tbody tr:not(.empty-row)'));
|
|
}
|
|
|
|
function baseObject(row) {
|
|
try { return JSON.parse(row.getAttribute('data-json') || '{}'); } catch (e) { return {}; }
|
|
}
|
|
|
|
function refreshEmpty(table) {
|
|
var empty = table.querySelector('.empty-row');
|
|
if (!empty) return;
|
|
empty.style.display = table.querySelectorAll('tbody tr:not(.empty-row)').length ? 'none' : '';
|
|
}
|
|
|
|
// Each card carries data-section; any edit inside it marks that section.
|
|
document.querySelectorAll('[data-section]').forEach(function (card) {
|
|
var section = card.getAttribute('data-section');
|
|
card.addEventListener('input', function () { markDirty(section); });
|
|
card.addEventListener('change', function () { markDirty(section); });
|
|
card.addEventListener('click', function (e) {
|
|
// app.js removes the row and renumbers (delegated on document, so it
|
|
// runs after this); refresh the empty row once it is gone.
|
|
if (e.target.closest('.remove-row')) {
|
|
markDirty(section);
|
|
var table = e.target.closest('table');
|
|
setTimeout(function () { refreshEmpty(table); }, 0);
|
|
}
|
|
});
|
|
});
|
|
|
|
var addBtn = document.getElementById('ospAddModel');
|
|
if (addBtn) {
|
|
addBtn.addEventListener('click', function () {
|
|
var tbody = document.querySelector('#ospModelTable tbody');
|
|
var tr = document.createElement('tr');
|
|
tr.className = 'row-new';
|
|
tr.innerHTML =
|
|
'<td class="num order-num"></td>' +
|
|
'<td><input type="text" class="form-control form-control-sm" data-field="Model" placeholder="Model name" aria-label="Model"></td>' +
|
|
'<td><input type="text" class="form-control form-control-sm" data-field="Id" placeholder="Driver family ID" aria-label="Family ID"></td>' +
|
|
'<td><span class="badge badge-info">New</span></td>' +
|
|
'<td class="text-end"><button type="button" class="btn btn-outline-danger btn-row-action remove-row" title="Remove this model"><i class="bi bi-trash"></i></button></td>';
|
|
tbody.insertBefore(tr, tbody.querySelector('.empty-row'));
|
|
rowsOf('ospModelTable').forEach(function (r, i) { r.querySelector('.order-num').textContent = i + 1; });
|
|
refreshEmpty(tbody.closest('table'));
|
|
markDirty('hardware_models');
|
|
tr.querySelector('input').focus();
|
|
});
|
|
}
|
|
|
|
function collect() {
|
|
var body = {};
|
|
if (dirty.operating_systems) {
|
|
body.operating_systems = rowsOf('ospOsTable').map(baseObject);
|
|
}
|
|
if (dirty.packages) {
|
|
// "enabled" keeps the file's own type: the MCL packages.json uses 1/0
|
|
// (PESetup reads it as a number), ours true/false. A row whose switch
|
|
// was not touched is sent exactly as it came.
|
|
var rows = rowsOf('ospPkgTable');
|
|
var numeric = rows.some(function (row) { return typeof baseObject(row).enabled === 'number'; });
|
|
body.packages = rows.map(function (row) {
|
|
var obj = baseObject(row);
|
|
var sw = row.querySelector('.pkg-enabled');
|
|
if (sw && sw.checked !== sw.defaultChecked) {
|
|
var asNumber = typeof obj.enabled === 'number' || (obj.enabled === undefined && numeric);
|
|
obj.enabled = asNumber ? (sw.checked ? 1 : 0) : sw.checked;
|
|
}
|
|
return obj;
|
|
});
|
|
}
|
|
if (dirty.hardware_models) {
|
|
// Keep each row's other keys and key order; only Model/Id change.
|
|
body.hardware_models = rowsOf('ospModelTable').map(function (row) {
|
|
var obj = row.getAttribute('data-json') ? baseObject(row) : { Id: '', Model: '' };
|
|
row.querySelectorAll('[data-field]').forEach(function (inp) {
|
|
obj[inp.getAttribute('data-field')] = inp.value.trim();
|
|
});
|
|
return obj;
|
|
});
|
|
}
|
|
return body;
|
|
}
|
|
|
|
reviewBtn.addEventListener('click', function () {
|
|
PxeImage.reviewAndSave({
|
|
title: 'Review changes: OS and Packages',
|
|
previewUrl: '{{ url_for("images.os_preview", image_type=image_type) }}',
|
|
saveUrl: '{{ url_for("images.os_save", image_type=image_type) }}',
|
|
body: collect(),
|
|
onSaved: function () { dirty = {}; window.location.reload(); }
|
|
});
|
|
});
|
|
|
|
window.addEventListener('beforeunload', function (e) {
|
|
if (Object.keys(dirty).length) { e.preventDefault(); e.returnValue = ''; }
|
|
});
|
|
})();
|
|
</script>
|
|
{% endblock %}
|