Add wimtools and startnet.cmd editor for boot.wim modification
- Added wimtools to offline packages and playbook verification - Webapp startnet.cmd editor: extract, view, edit, save back to boot.wim - Uses wimextract/wimupdate for in-place WIM modification - Dark-themed code editor with tab support and common command reference Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ PLAYBOOK_PACKAGES=(
|
|||||||
unzip
|
unzip
|
||||||
ufw
|
ufw
|
||||||
cron
|
cron
|
||||||
|
wimtools
|
||||||
)
|
)
|
||||||
|
|
||||||
# Packages installed during autoinstall late-commands (NetworkManager, WiFi, etc.)
|
# Packages installed during autoinstall late-commands (NetworkManager, WiFi, etc.)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
- ufw
|
- ufw
|
||||||
- cron
|
- cron
|
||||||
- ansible
|
- ansible
|
||||||
|
- wimtools
|
||||||
register: pkg_check
|
register: pkg_check
|
||||||
failed_when: false
|
failed_when: false
|
||||||
changed_when: false
|
changed_when: false
|
||||||
@@ -374,6 +375,7 @@
|
|||||||
WorkingDirectory=/opt/pxe-webapp
|
WorkingDirectory=/opt/pxe-webapp
|
||||||
Environment=SAMBA_SHARE={{ samba_share }}
|
Environment=SAMBA_SHARE={{ samba_share }}
|
||||||
Environment=CLONEZILLA_SHARE=/srv/samba/clonezilla
|
Environment=CLONEZILLA_SHARE=/srv/samba/clonezilla
|
||||||
|
Environment=WEB_ROOT={{ web_root }}
|
||||||
ExecStart=/opt/pxe-webapp/venv/bin/python app.py
|
ExecStart=/opt/pxe-webapp/venv/bin/python app.py
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
|
|||||||
114
webapp/app.py
114
webapp/app.py
@@ -29,6 +29,8 @@ app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024 * 1024 # 16 GB max upload
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
SAMBA_SHARE = os.environ.get("SAMBA_SHARE", "/srv/samba/winpeapps")
|
SAMBA_SHARE = os.environ.get("SAMBA_SHARE", "/srv/samba/winpeapps")
|
||||||
CLONEZILLA_SHARE = os.environ.get("CLONEZILLA_SHARE", "/srv/samba/clonezilla")
|
CLONEZILLA_SHARE = os.environ.get("CLONEZILLA_SHARE", "/srv/samba/clonezilla")
|
||||||
|
WEB_ROOT = os.environ.get("WEB_ROOT", "/var/www/html")
|
||||||
|
BOOT_WIM = os.path.join(WEB_ROOT, "win11", "sources", "boot.wim")
|
||||||
|
|
||||||
IMAGE_TYPES = [
|
IMAGE_TYPES = [
|
||||||
"gea-standard",
|
"gea-standard",
|
||||||
@@ -638,6 +640,118 @@ def clonezilla_delete(filename):
|
|||||||
return redirect(url_for("clonezilla_backups"))
|
return redirect(url_for("clonezilla_backups"))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Routes — startnet.cmd Editor (WIM)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _wim_extract_startnet(wim_path):
|
||||||
|
"""Extract startnet.cmd from a WIM file using wimextract."""
|
||||||
|
import tempfile
|
||||||
|
tmpdir = tempfile.mkdtemp()
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["wimextract", wim_path, "1",
|
||||||
|
"/Windows/System32/startnet.cmd",
|
||||||
|
"--dest-dir", tmpdir],
|
||||||
|
capture_output=True, text=True, timeout=30,
|
||||||
|
)
|
||||||
|
startnet_path = os.path.join(tmpdir, "startnet.cmd")
|
||||||
|
if result.returncode == 0 and os.path.isfile(startnet_path):
|
||||||
|
content = open(startnet_path, "r", encoding="utf-8", errors="replace").read()
|
||||||
|
return content
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _wim_update_startnet(wim_path, content):
|
||||||
|
"""Update startnet.cmd inside a WIM file using wimupdate."""
|
||||||
|
import tempfile
|
||||||
|
tmpdir = tempfile.mkdtemp()
|
||||||
|
try:
|
||||||
|
startnet_path = os.path.join(tmpdir, "startnet.cmd")
|
||||||
|
with open(startnet_path, "w", encoding="utf-8", newline="\r\n") as fh:
|
||||||
|
fh.write(content)
|
||||||
|
# wimupdate reads commands from stdin
|
||||||
|
update_cmd = f"add {startnet_path} /Windows/System32/startnet.cmd\n"
|
||||||
|
result = subprocess.run(
|
||||||
|
["wimupdate", wim_path, "1"],
|
||||||
|
input=update_cmd,
|
||||||
|
capture_output=True, text=True, timeout=60,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return False, result.stderr.strip()
|
||||||
|
return True, ""
|
||||||
|
except Exception as exc:
|
||||||
|
return False, str(exc)
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _wim_list_files(wim_path, path="/"):
|
||||||
|
"""List files inside a WIM at the given path."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["wimdir", wim_path, "1", path],
|
||||||
|
capture_output=True, text=True, timeout=30,
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
return [l.strip() for l in result.stdout.splitlines() if l.strip()]
|
||||||
|
return []
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/startnet")
|
||||||
|
def startnet_editor():
|
||||||
|
wim_exists = os.path.isfile(BOOT_WIM)
|
||||||
|
content = ""
|
||||||
|
wim_info = {}
|
||||||
|
|
||||||
|
if wim_exists:
|
||||||
|
content = _wim_extract_startnet(BOOT_WIM) or ""
|
||||||
|
# Get WIM info
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["wiminfo", BOOT_WIM],
|
||||||
|
capture_output=True, text=True, timeout=15,
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
for line in result.stdout.splitlines():
|
||||||
|
if ":" in line:
|
||||||
|
key, _, val = line.partition(":")
|
||||||
|
wim_info[key.strip()] = val.strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"startnet_editor.html",
|
||||||
|
wim_exists=wim_exists,
|
||||||
|
wim_path=BOOT_WIM,
|
||||||
|
content=content,
|
||||||
|
wim_info=wim_info,
|
||||||
|
image_types=IMAGE_TYPES,
|
||||||
|
friendly_names=FRIENDLY_NAMES,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/startnet/save", methods=["POST"])
|
||||||
|
def startnet_save():
|
||||||
|
if not os.path.isfile(BOOT_WIM):
|
||||||
|
flash("boot.wim not found.", "danger")
|
||||||
|
return redirect(url_for("startnet_editor"))
|
||||||
|
|
||||||
|
content = request.form.get("content", "")
|
||||||
|
ok, err = _wim_update_startnet(BOOT_WIM, content)
|
||||||
|
if ok:
|
||||||
|
flash("startnet.cmd updated successfully in boot.wim.", "success")
|
||||||
|
else:
|
||||||
|
flash(f"Failed to update boot.wim: {err}", "danger")
|
||||||
|
return redirect(url_for("startnet_editor"))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Routes — API
|
# Routes — API
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -137,6 +137,12 @@
|
|||||||
<div class="nav-section-divider"></div>
|
<div class="nav-section-divider"></div>
|
||||||
<div class="sidebar-heading">Tools</div>
|
<div class="sidebar-heading">Tools</div>
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.endpoint == 'startnet_editor' %}active{% endif %}"
|
||||||
|
href="{{ url_for('startnet_editor') }}">
|
||||||
|
<i class="bi bi-terminal"></i> startnet.cmd
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.endpoint == 'clonezilla_backups' %}active{% endif %}"
|
<a class="nav-link {% if request.endpoint == 'clonezilla_backups' %}active{% endif %}"
|
||||||
href="{{ url_for('clonezilla_backups') }}">
|
href="{{ url_for('clonezilla_backups') }}">
|
||||||
|
|||||||
127
webapp/templates/startnet_editor.html
Normal file
127
webapp/templates/startnet_editor.html
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}startnet.cmd Editor - PXE Server Manager{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_head %}
|
||||||
|
<style>
|
||||||
|
.cmd-editor {
|
||||||
|
font-family: 'Consolas', 'Courier New', monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
min-height: 400px;
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
border: 1px solid #333;
|
||||||
|
padding: 1rem;
|
||||||
|
tab-size: 4;
|
||||||
|
white-space: pre;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.cmd-editor:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #0d6efd;
|
||||||
|
box-shadow: 0 0 0 0.15rem rgba(13,110,253,.25);
|
||||||
|
}
|
||||||
|
.wim-info dt {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
.wim-info dd {
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="mb-0"><i class="bi bi-terminal me-2"></i>startnet.cmd Editor</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if not wim_exists %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||||
|
<strong>boot.wim not found</strong> at <code>{{ wim_path }}</code>.
|
||||||
|
Run the PXE server setup playbook and import WinPE boot files first.
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-9">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<span><i class="bi bi-file-earmark-code me-2"></i>Windows\System32\startnet.cmd</span>
|
||||||
|
<span class="badge bg-secondary">boot.wim</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<form action="{{ url_for('startnet_save') }}" method="post" id="startnetForm">
|
||||||
|
<textarea name="content" class="form-control cmd-editor" id="cmdEditor"
|
||||||
|
spellcheck="false">{{ content }}</textarea>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||||
|
<small class="text-muted">
|
||||||
|
Editing the startnet.cmd inside <code>{{ wim_path }}</code>
|
||||||
|
</small>
|
||||||
|
<button type="submit" form="startnetForm" class="btn btn-primary">
|
||||||
|
<i class="bi bi-floppy me-1"></i> Save to boot.wim
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title"><i class="bi bi-info-circle me-1"></i> Common startnet.cmd Commands</h6>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<code class="d-block mb-1">wpeinit</code>
|
||||||
|
<small class="text-muted d-block mb-2">Initialize WinPE networking</small>
|
||||||
|
<code class="d-block mb-1">net use Z: \\10.9.100.1\winpeapps</code>
|
||||||
|
<small class="text-muted d-block mb-2">Map Samba share for deployment</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<code class="d-block mb-1">wpeutil WaitForNetwork</code>
|
||||||
|
<small class="text-muted d-block mb-2">Wait for network to be ready</small>
|
||||||
|
<code class="d-block mb-1">Z:\gea-standard\Deploy\Tools\deploy.cmd</code>
|
||||||
|
<small class="text-muted d-block mb-2">Launch deployment script</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<i class="bi bi-info-square me-2"></i>WIM Info
|
||||||
|
</div>
|
||||||
|
<div class="card-body wim-info">
|
||||||
|
<dl class="mb-0">
|
||||||
|
{% for key, val in wim_info.items() %}
|
||||||
|
{% if key in ['Image Count', 'Compression', 'Total Bytes', 'Image Name', 'Image Description'] %}
|
||||||
|
<dt>{{ key }}</dt>
|
||||||
|
<dd>{{ val }}</dd>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if not wim_info %}
|
||||||
|
<p class="text-muted mb-0">Could not read WIM info.</p>
|
||||||
|
{% endif %}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_scripts %}
|
||||||
|
<script>
|
||||||
|
// Tab key inserts a tab in the editor instead of moving focus
|
||||||
|
document.getElementById('cmdEditor')?.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
var start = this.selectionStart;
|
||||||
|
var end = this.selectionEnd;
|
||||||
|
this.value = this.value.substring(0, start) + '\t' + this.value.substring(end);
|
||||||
|
this.selectionStart = this.selectionEnd = start + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user