From 89b58347d956f1d5713d6dd62e2ace82f4dcf860 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Fri, 6 Feb 2026 16:23:22 -0500 Subject: [PATCH] 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 --- download-packages.sh | 1 + playbook/pxe_server_setup.yml | 2 + webapp/app.py | 114 +++++++++++++++++++++++ webapp/templates/base.html | 6 ++ webapp/templates/startnet_editor.html | 127 ++++++++++++++++++++++++++ 5 files changed, 250 insertions(+) create mode 100644 webapp/templates/startnet_editor.html diff --git a/download-packages.sh b/download-packages.sh index 9c92a15..33084e3 100755 --- a/download-packages.sh +++ b/download-packages.sh @@ -25,6 +25,7 @@ PLAYBOOK_PACKAGES=( unzip ufw cron + wimtools ) # Packages installed during autoinstall late-commands (NetworkManager, WiFi, etc.) diff --git a/playbook/pxe_server_setup.yml b/playbook/pxe_server_setup.yml index b6fac48..4f98dd6 100644 --- a/playbook/pxe_server_setup.yml +++ b/playbook/pxe_server_setup.yml @@ -16,6 +16,7 @@ - ufw - cron - ansible + - wimtools register: pkg_check failed_when: false changed_when: false @@ -374,6 +375,7 @@ WorkingDirectory=/opt/pxe-webapp Environment=SAMBA_SHARE={{ samba_share }} Environment=CLONEZILLA_SHARE=/srv/samba/clonezilla + Environment=WEB_ROOT={{ web_root }} ExecStart=/opt/pxe-webapp/venv/bin/python app.py Restart=always RestartSec=5 diff --git a/webapp/app.py b/webapp/app.py index 51e6239..bf6722a 100644 --- a/webapp/app.py +++ b/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") 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 = [ "gea-standard", @@ -638,6 +640,118 @@ def clonezilla_delete(filename): 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 # --------------------------------------------------------------------------- diff --git a/webapp/templates/base.html b/webapp/templates/base.html index 3ccf728..53e55cb 100644 --- a/webapp/templates/base.html +++ b/webapp/templates/base.html @@ -137,6 +137,12 @@