diff --git a/chromium-setup/KDE-INSTALLATION-GUIDE.md b/chromium-setup/KDE-INSTALLATION-GUIDE.md index 0e56f84..06c87b9 100644 --- a/chromium-setup/KDE-INSTALLATION-GUIDE.md +++ b/chromium-setup/KDE-INSTALLATION-GUIDE.md @@ -113,6 +113,16 @@ ps aux | grep -i plasma --- +## Cloud-Init (Automated Install) + +For EMMC provisioning you can install KDE, set it as the default session, and enable touch options automatically using cloud-init. Use the example that includes KDE and touch: + +- **File:** `emmc-provisioning/cloud-init/user-data-kiosk-username-ssh.example` + +It installs `kde-plasma-desktop`, `maliit-keyboard`, and `xinput-calibrator`; sets LightDM default session to Plasma (X11); writes KDE config for touch-friendly scaling and touch-point feedback; and autostarts the on-screen keyboard. Chromium kiosk and SSH are configured in the same example. Replace the password hash and use it as your cloud-init `user-data` when building the image. + +--- + ## Reverting to LXDE ### Method 1: Switch at Login Screen (Easiest) diff --git a/chromium-setup/emmc-provisioning/cloud-init/user-data-kiosk-username-ssh.example b/chromium-setup/emmc-provisioning/cloud-init/user-data-kiosk-username-ssh.example new file mode 100644 index 0000000..1d6b085 --- /dev/null +++ b/chromium-setup/emmc-provisioning/cloud-init/user-data-kiosk-username-ssh.example @@ -0,0 +1,118 @@ +#cloud-config +# Example: create user (pi) with password, enable SSH, install KDE Plasma with touch options, +# set KDE as default GUI, and deploy Chromium kiosk autostart. +# Uses start-chromium.sh and chromium-kiosk.desktop from this project (chromium-setup/). +# +# 1. Generate a password hash on a Linux host: +# mkpasswd -m sha-512 'YourPassword' +# or: openssl passwd -6 'YourPassword' +# Paste the full output (e.g. $6$...) into the passwd: line below. +# 2. To use a different username than "pi", replace every "pi" in this file. +# 3. To change the kiosk URL, edit the --app=... line in the start-chromium.sh content below. + +package_update: true +package_upgrade: false + +packages: + - chromium-browser + - wmctrl + - openssh-server + # KDE Plasma + touchscreen + - kde-plasma-desktop + - maliit-keyboard + - xinput-calibrator + +# Create user and set password (use hash from mkpasswd -m sha-512 or openssl passwd -6) +users: + - name: pi + groups: [adm, sudo, video] + lock_passwd: false + passwd: "$6$7xWGhGc6d1lJx1dU$4E8r1mkzVj51bjEbfzdP8wPxso..C36LbXkqU/X4oBGq94aGFMSrZb0PVI8zs/Om1Jm97/D..Apy2HTdCn3FV1" + shell: /bin/bash + +# Enable SSH (allow password auth so you can log in with the user above) +write_files: + - path: /etc/ssh/sshd_config.d/99-cloud-init.conf + content: | + PasswordAuthentication yes + PermitRootLogin no + - path: /home/pi/start-chromium.sh + content: | + #!/bin/bash + export GNOME_KEYRING_CONTROL="" + export DISPLAY=:0 + export GDK_BACKEND=x11 + unset WAYLAND_DISPLAY + for i in {1..60}; do + if xset q >/dev/null 2>&1 || [ -n "$DISPLAY" ]; then + if pgrep -x plasma_session >/dev/null 2>&1 || pgrep -x kwin_x11 >/dev/null 2>&1 || pgrep -x pcmanfm >/dev/null 2>&1 || pgrep -x lxsession >/dev/null 2>&1; then + break + fi + fi + sleep 0.5 + done + sleep 5 + /usr/bin/chromium --start-fullscreen --noerrdialogs --disable-infobars --disable-session-crashed-bubble --disable-restore-session-state --no-first-run --password-store=basic --use-mock-keychain --ozone-platform=x11 --disable-features=UseChromeOSDirectVideoDecoder --app=http://127.0.0.1:8080 & + sleep 3 + for i in {1..10}; do + WINDOW_ID=$(wmctrl -l 2>/dev/null | grep -i chromium | head -1 | awk '{print $1}') + if [ -n "$WINDOW_ID" ]; then + wmctrl -i -r "$WINDOW_ID" -b add,fullscreen 2>/dev/null + break + fi + sleep 0.5 + done + wait + owner: pi:pi + permissions: "0755" + - path: /home/pi/.config/autostart/chromium-kiosk.desktop + content: | + [Desktop Entry] + Type=Application + Name=Chromium Fullscreen + Exec=/home/pi/start-chromium.sh + Hidden=false + NoDisplay=false + X-GNOME-Autostart-enabled=true + owner: pi:pi + permissions: "0644" + # KDE Plasma: switch to KDE as default session (X11 for Chromium compatibility) + - path: /etc/lightdm/lightdm.conf.d/99-default-session.conf + content: | + [Seat:*] + user-session=plasmax11 + permissions: "0644" + # KDE touch-friendly: UI scale and input (for pi after first login) + - path: /home/pi/.config/kdeglobals + content: | + [General] + ForceFontDPI=120 + owner: pi:pi + permissions: "0644" + - path: /home/pi/.config/kwinrc + content: | + [Windows] + BorderlessMaximizedWindows=true + [Plugins] + touchpointsEnabled=true + owner: pi:pi + permissions: "0644" + # Start on-screen keyboard (maliit) with KDE for touch input + - path: /home/pi/.config/autostart/maliit-keyboard.desktop + content: | + [Desktop Entry] + Type=Application + Name=Maliit Keyboard + Exec=maliit-keyboard -r + X-GNOME-Autostart-enabled=true + owner: pi:pi + permissions: "0644" + +runcmd: + - mkdir -p /home/pi/.config/autostart + - chown -R pi:pi /home/pi/.config + # Set KDE Plasma (X11) as default session so next boot uses KDE + - update-alternatives --set x-session-manager /usr/bin/startplasma-x11 2>/dev/null || true + - systemctl enable ssh + - systemctl start ssh + - cloud-init single --name cc_final_message diff --git a/chromium-setup/emmc-provisioning/dashboard/app.py b/chromium-setup/emmc-provisioning/dashboard/app.py index 7ac74b4..77903ac 100644 --- a/chromium-setup/emmc-provisioning/dashboard/app.py +++ b/chromium-setup/emmc-provisioning/dashboard/app.py @@ -38,6 +38,8 @@ GOLDEN_IMAGE = Path(os.environ.get("CM4_GOLDEN_IMAGE", str(BASE_DIR / "golden.im NETWORK_DEVICES_FILE = Path(os.environ.get("CM4_NETWORK_DEVICES_FILE", str(BASE_DIR / "network_devices.json"))) BUILD_STATUS_FILE = Path(os.environ.get("CM4_BUILD_STATUS_FILE", str(BASE_DIR / "build_cloudinit_status.json"))) BUILD_REQUEST_FILE = Path(os.environ.get("CM4_BUILD_REQUEST_FILE", str(BASE_DIR / "build_cloudinit_request.json"))) +SHRINK_REQUEST_FILE = Path(os.environ.get("CM4_SHRINK_REQUEST_FILE", str(BASE_DIR / "shrink_request.json"))) +SHRINK_STATUS_FILE = Path(os.environ.get("CM4_SHRINK_STATUS_FILE", str(BASE_DIR / "shrink_status.json"))) CLOUDINIT_TEMPLATES_FILE = Path(os.environ.get("CM4_CLOUDINIT_TEMPLATES_FILE", str(BASE_DIR / "cloudinit_templates.json"))) # Default cloud-init user-data for Raspberry Pi OS (NoCloud on boot partition) @@ -349,9 +351,37 @@ def api_backup_set_as_golden(name): return jsonify({"ok": False, "error": str(e)}), 500 +def _request_host_shrink(name, action="shrink", format="xz"): + """Write shrink request for host and poll shrink_status.json. Returns (ok, message_or_error).""" + req = {"name": name, "action": action} + if action == "compress": + req["format"] = "gz" if format == "gz" else "xz" + try: + SHRINK_REQUEST_FILE.write_text(json.dumps(req)) + except OSError as e: + return False, str(e) + deadline = time.monotonic() + 2100 # 35 min + while time.monotonic() < deadline: + time.sleep(5) + if not SHRINK_STATUS_FILE.exists(): + continue + try: + data = json.loads(SHRINK_STATUS_FILE.read_text()) + except (OSError, ValueError): + continue + if data.get("name") != name: + continue + phase = data.get("phase") + if phase == "done": + return True, data.get("message") or f"Shrunk {name}" + if phase == "error": + return False, data.get("error") or "PiShrink failed" + return False, "Shrink timed out (run on host may still be in progress)" + + @app.route("/api/backups//shrink", methods=["POST"]) def api_backup_shrink(name): - """Run PiShrink on a raw .img backup (shrinks in place). Requires PiShrink in LXC/host.""" + """Request PiShrink on host for a raw .img backup (shrinks in place). Dashboard polls host status.""" if not _safe_backup_name(name): return jsonify({"ok": False, "error": "invalid backup name"}), 400 if not name.endswith(".img") or name.endswith(".img.gz") or name.endswith(".img.xz"): @@ -359,33 +389,15 @@ def api_backup_shrink(name): path = BACKUPS_DIR / name if not path.is_file(): return jsonify({"ok": False, "error": "backup not found"}), 404 - pishrink = shutil.which("pishrink.sh") or "/usr/local/bin/pishrink.sh" - if not pishrink or not os.path.isfile(pishrink): - return jsonify({"ok": False, "error": "PiShrink not installed. Install on the host/LXC (e.g. scripts/install-pishrink-on-host.sh)"}), 503 - try: - proc = subprocess.run( - [pishrink, "-n", name], - cwd=str(BACKUPS_DIR), - capture_output=True, - text=True, - timeout=3600, - ) - if proc.returncode != 0: - return jsonify({ - "ok": False, - "error": "PiShrink failed", - "detail": (proc.stderr or proc.stdout or "").strip() or f"exit code {proc.returncode}", - }), 500 - return jsonify({"ok": True, "message": f"Shrunk {name}"}) - except subprocess.TimeoutExpired: - return jsonify({"ok": False, "error": "PiShrink timed out"}), 504 - except OSError as e: - return jsonify({"ok": False, "error": str(e)}), 500 + ok, msg = _request_host_shrink(name, action="shrink") + if ok: + return jsonify({"ok": True, "message": msg}) + return jsonify({"ok": False, "error": msg}), (503 if "not installed" in msg else 504 if "timed out" in msg else 500) @app.route("/api/backups//compress", methods=["POST"]) def api_backup_compress(name): - """Run PiShrink with compression (shrink + gz or xz) on a raw .img backup. Produces .img.gz or .img.xz.""" + """Request PiShrink with compression on host. Produces .img.gz or .img.xz. Dashboard polls host status.""" if not _safe_backup_name(name): return jsonify({"ok": False, "error": "invalid backup name"}), 400 if not name.endswith(".img") or name.endswith(".img.gz") or name.endswith(".img.xz"): @@ -399,30 +411,11 @@ def api_backup_compress(name): fmt = "xz" if fmt == "gzip": fmt = "gz" - pishrink = shutil.which("pishrink.sh") or "/usr/local/bin/pishrink.sh" - if not pishrink or not os.path.isfile(pishrink): - return jsonify({"ok": False, "error": "PiShrink not installed"}), 503 - opts = ["-n", "-Z", "-a"] if fmt == "xz" else ["-n", "-z", "-a"] - try: - proc = subprocess.run( - [pishrink] + opts + [name], - cwd=str(BACKUPS_DIR), - capture_output=True, - text=True, - timeout=3600, - ) - if proc.returncode != 0: - return jsonify({ - "ok": False, - "error": "PiShrink failed", - "detail": (proc.stderr or proc.stdout or "").strip() or f"exit code {proc.returncode}", - }), 500 + ok, msg = _request_host_shrink(name, action="compress", format=fmt) + if ok: ext = ".xz" if fmt == "xz" else ".gz" - return jsonify({"ok": True, "message": f"Compressed to {name}{ext}"}) - except subprocess.TimeoutExpired: - return jsonify({"ok": False, "error": "PiShrink timed out"}), 504 - except OSError as e: - return jsonify({"ok": False, "error": str(e)}), 500 + return jsonify({"ok": True, "message": msg or f"Compressed to {name}{ext}"}) + return jsonify({"ok": False, "error": msg}), (503 if "not installed" in msg else 504 if "timed out" in msg else 500) @app.route("/api/backups/", methods=["PATCH"]) @@ -504,20 +497,26 @@ def _raspios_latest_url(variant="lite"): """Resolve latest Raspberry Pi OS (arm64) .img.xz URL. variant=lite|full.""" slug = "raspios_lite_arm64" if variant == "lite" else "raspios_full_arm64" base = f"https://downloads.raspberrypi.com/{slug}/images" + headers = {"User-Agent": "Mozilla/5.0 (compatible; CM4-Provisioning/1.0)"} try: - with urllib.request.urlopen(base + "/", timeout=15) as r: + req = urllib.request.Request(base + "/", headers=headers) + with urllib.request.urlopen(req, timeout=20) as r: html = r.read().decode("utf-8", errors="ignore") folders = re.findall(rf"{re.escape(slug)}-(\d{{4}}-\d{{2}}-\d{{2}})/", html) if not folders: return None latest = sorted(folders)[-1] folder_url = f"{base}/{slug}-{latest}/" - with urllib.request.urlopen(folder_url, timeout=15) as r: + req2 = urllib.request.Request(folder_url, headers=headers) + with urllib.request.urlopen(req2, timeout=20) as r: folder_html = r.read().decode("utf-8", errors="ignore") m = re.search(r'href="([^"]+\.img\.xz)"', folder_html) if not m: return None - return folder_url + m.group(1) + href = m.group(1) + if href.startswith("http://") or href.startswith("https://"): + return href + return folder_url.rstrip("/") + "/" + href.lstrip("/") except Exception: return None diff --git a/chromium-setup/emmc-provisioning/docs/EMMC-PROVISIONING-GUIDE.md b/chromium-setup/emmc-provisioning/docs/EMMC-PROVISIONING-GUIDE.md index 65f7cfc..6eb68c9 100644 --- a/chromium-setup/emmc-provisioning/docs/EMMC-PROVISIONING-GUIDE.md +++ b/chromium-setup/emmc-provisioning/docs/EMMC-PROVISIONING-GUIDE.md @@ -227,6 +227,7 @@ Raw full-disk backups and golden images are the full size of the eMMC (e.g. 32 - PiShrink only shrinks the **last** partition; it must be ext2/3/4 (standard Raspberry Pi OS root is ext4). - Compressed backups (`.img.gz` / `.img.xz`) are for archival; to use as golden image, decompress first (e.g. `gunzip -k backup.img.gz` then copy to `golden.img`). +- **Dashboard "Shrink" / "Compress"** buttons run PiShrink **on the host** (not in the LXC). The dashboard writes a request file; the host runs `run-shrink-on-host.sh` when `cm4-shrink.path` sees it. Ensure PiShrink is installed on the host (see above) and that deploy has installed `run-shrink-on-host.sh` and enabled `cm4-shrink.path`. ### Cloud-init file locations on the Pi diff --git a/chromium-setup/emmc-provisioning/docs/PROXMOX-LXC-DEPLOYMENT.md b/chromium-setup/emmc-provisioning/docs/PROXMOX-LXC-DEPLOYMENT.md index 37b5c73..68e9893 100644 --- a/chromium-setup/emmc-provisioning/docs/PROXMOX-LXC-DEPLOYMENT.md +++ b/chromium-setup/emmc-provisioning/docs/PROXMOX-LXC-DEPLOYMENT.md @@ -195,9 +195,13 @@ Or copy `scripts/monitor-from-host.sh` to the host and run `./monitor-from-host. 4. **Clear stuck error in portal** – If the portal shows an old error (e.g. "Golden image not found" or "rpiboot failed"), click **Clear message** in the dashboard, or: `ssh root@10.130.60.224 "echo '{\"phase\":\"idle\",\"message\":\"Waiting for reTerminal in boot mode or network.\",\"progress\":null}' > /var/lib/cm4-provisioning/status.json"`. Then unplug/replug the device. -5. **Backup stops before finishing** – If backup or shrink appears to stop partway (e.g. dashboard stuck on "Creating backup…" or "Shrinking…"), the service may have been killed by systemd. The `cm4-flash.service` unit uses `TimeoutStartSec=7200` (2 hours); if you deployed an older version with 15 minutes, redeploy so the host gets the updated unit, then on the host run `systemctl daemon-reload` so the next backup has enough time to complete. +5. **"PiShrink not installed" when clicking Shrink/Compress** – Shrink and Compress run **on the host**, not in the LXC. Install PiShrink on the host: `ssh root@HOST 'bash -s' < chromium-setup/emmc-provisioning/scripts/install-pishrink-on-host.sh`. Ensure deploy has been run so the host has `run-shrink-on-host.sh` and `cm4-shrink.path` enabled (`systemctl status cm4-shrink.path`). -6. **Trigger now runs the flash script in the background** (not via systemd-run) so it can access the USB device; a 2s delay gives the device time to enumerate before rpiboot runs. +6. **"Download failed" when building cloud-init image** – The download runs on the **host** (not the LXC). Check: (1) Host can reach the internet: `ssh root@HOST 'curl -sI https://downloads.raspberrypi.com/'`. (2) Build status now shows curl’s error (e.g. "Could not resolve host", "Connection timed out"); check the dashboard error text. (3) If you use a proxy or custom CA, set in `/opt/cm4-provisioning/env`: `CURL_INSECURE=1` to skip SSL verify (only if you understand the risk), then rerun the build. + +7. **Backup stops before finishing** – If backup or shrink appears to stop partway (e.g. dashboard stuck on "Creating backup…" or "Shrinking…"), the service may have been killed by systemd. The `cm4-flash.service` unit uses `TimeoutStartSec=7200` (2 hours); if you deployed an older version with 15 minutes, redeploy so the host gets the updated unit, then on the host run `systemctl daemon-reload` so the next backup has enough time to complete. + +8. **Trigger now runs the flash script in the background** (not via systemd-run) so it can access the USB device; a 2s delay gives the device time to enumerate before rpiboot runs. --- diff --git a/chromium-setup/emmc-provisioning/host/build-cloudinit-image.sh b/chromium-setup/emmc-provisioning/host/build-cloudinit-image.sh index ddccf8c..26cf8da 100644 --- a/chromium-setup/emmc-provisioning/host/build-cloudinit-image.sh +++ b/chromium-setup/emmc-provisioning/host/build-cloudinit-image.sh @@ -53,8 +53,12 @@ OUT_PATH="$BACKUPS_DIR/$OUT_NAME" write_status "downloading" "Downloading $(basename "$URL")…" "" "" XZ_FILE="$TEMP_DIR/image.img.xz" -if ! curl -f -sS -L -o "$XZ_FILE" "$URL"; then - write_status "error" "" "" "Download failed" +CURL_ERR="$TEMP_DIR/curl_err.txt" +CURL_OPTS=(-f -L -o "$XZ_FILE" --connect-timeout 30 --max-time 7200) +[[ "${CURL_INSECURE:-}" == "1" ]] && CURL_OPTS+=(-k) +if ! curl "${CURL_OPTS[@]}" "$URL" 2>"$CURL_ERR"; then + err_detail=$(head -c 200 "$CURL_ERR" | tr '\n' ' ' | sed 's/"/\\"/g') + write_status "error" "" "" "Download failed: ${err_detail:-curl exited with error}" exit 1 fi diff --git a/chromium-setup/emmc-provisioning/host/cm4-shrink.path b/chromium-setup/emmc-provisioning/host/cm4-shrink.path new file mode 100644 index 0000000..0c78224 --- /dev/null +++ b/chromium-setup/emmc-provisioning/host/cm4-shrink.path @@ -0,0 +1,9 @@ +[Unit] +Description=CM4 shrink/compress backup (when request file appears) +After=local-fs.target + +[Path] +PathExists=/var/lib/cm4-provisioning/shrink_request.json + +[Install] +WantedBy=multi-user.target diff --git a/chromium-setup/emmc-provisioning/host/cm4-shrink.service b/chromium-setup/emmc-provisioning/host/cm4-shrink.service new file mode 100644 index 0000000..b8cce66 --- /dev/null +++ b/chromium-setup/emmc-provisioning/host/cm4-shrink.service @@ -0,0 +1,12 @@ +[Unit] +Description=CM4 run PiShrink on requested backup image +After=local-fs.target + +[Service] +Type=oneshot +Environment=CM4_PROVISIONING_DIR=/var/lib/cm4-provisioning +ExecStart=/opt/cm4-provisioning/run-shrink-on-host.sh +User=root +StandardOutput=journal +StandardError=journal +TimeoutStartSec=3600 diff --git a/chromium-setup/emmc-provisioning/host/run-shrink-on-host.sh b/chromium-setup/emmc-provisioning/host/run-shrink-on-host.sh new file mode 100644 index 0000000..934c094 --- /dev/null +++ b/chromium-setup/emmc-provisioning/host/run-shrink-on-host.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Run on the Proxmox host when shrink_request.json appears in the provisioning dir. +# Runs PiShrink on the requested backup image (shrink or shrink+compress). +# Requires PiShrink on host (scripts/install-pishrink-on-host.sh). Triggered by cm4-shrink.path. + +set -e +PROV_DIR="${CM4_PROVISIONING_DIR:-/var/lib/cm4-provisioning}" +REQUEST_FILE="$PROV_DIR/shrink_request.json" +STATUS_FILE="$PROV_DIR/shrink_status.json" +[[ -f /opt/cm4-provisioning/env ]] && source /opt/cm4-provisioning/env +BACKUPS_DIR="${BACKUPS_DIR:-$PROV_DIR/backups}" +PISHRINK="${PISHRINK_SCRIPT:-/usr/local/bin/pishrink.sh}" +SHRINK_TIMEOUT="${SHRINK_TIMEOUT:-2100}" + +write_status() { + local phase="$1" name="$2" message="$3" error="$4" + printf '{"phase":"%s","name":"%s","message":"%s","error":%s,"updated":%s}\n' \ + "$phase" "$name" "$message" \ + "$([ -n "$error" ] && echo "\"${error//\"/\\\"}\"" || echo "null")" \ + "$(date +%s)" > "$STATUS_FILE" +} + +[[ -f "$REQUEST_FILE" ]] || { echo "No request file"; exit 0; } + +# Parse request +NAME=$(python3 -c "import json; print(json.load(open('$REQUEST_FILE')).get('name',''))") +ACTION=$(python3 -c "import json; print(json.load(open('$REQUEST_FILE')).get('action','shrink'))") +FORMAT=$(python3 -c "import json; print(json.load(open('$REQUEST_FILE')).get('format','xz'))") +rm -f "$REQUEST_FILE" + +# Validate +if [[ -z "$NAME" ]]; then + write_status "error" "" "" "Missing name in request" + exit 0 +fi +if [[ "$NAME" != "${NAME##*/}" ]] || [[ "$NAME" == *..* ]]; then + write_status "error" "$NAME" "" "Invalid name" + exit 0 +fi +if [[ ! "$NAME" =~ \.img$ ]]; then + write_status "error" "$NAME" "" "Only .img files can be shrunk" + exit 0 +fi +PATH_FILE="$BACKUPS_DIR/$NAME" +if [[ ! -f "$PATH_FILE" ]]; then + write_status "error" "$NAME" "" "File not found" + exit 0 +fi + +if [[ ! -x "$PISHRINK" ]]; then + write_status "error" "$NAME" "" "PiShrink not installed on host. Run scripts/install-pishrink-on-host.sh" + exit 0 +fi + +write_status "running" "$NAME" "Shrinking…" "" +OPTS=(-n) +if [[ "$ACTION" == "compress" ]]; then + if [[ "$FORMAT" == "gz" ]] || [[ "$FORMAT" == "gzip" ]]; then + OPTS+=(-z -a) + else + OPTS+=(-Z -a) + fi +fi + +cd "$BACKUPS_DIR" || exit 1 +if timeout "$SHRINK_TIMEOUT" "$PISHRINK" "${OPTS[@]}" "$NAME" 2>&1; then + if [[ "$ACTION" == "compress" ]]; then + ext=".xz" + [[ "$FORMAT" == "gz" ]] || [[ "$FORMAT" == "gzip" ]] && ext=".gz" + write_status "done" "$NAME" "Compressed to ${NAME}${ext}" "" + else + write_status "done" "$NAME" "Shrunk $NAME" "" + fi +else + rc=$? + write_status "error" "$NAME" "" "PiShrink failed (exit $rc)" +fi diff --git a/chromium-setup/emmc-provisioning/scripts/deploy-to-proxmox.sh b/chromium-setup/emmc-provisioning/scripts/deploy-to-proxmox.sh index 26847bd..0eea037 100755 --- a/chromium-setup/emmc-provisioning/scripts/deploy-to-proxmox.sh +++ b/chromium-setup/emmc-provisioning/scripts/deploy-to-proxmox.sh @@ -75,13 +75,18 @@ cp "$DEPLOY/host/flash-emmc-on-connect.sh" /opt/cm4-provisioning/ chmod +x /opt/cm4-provisioning/flash-emmc-on-connect.sh cp "$DEPLOY/host/build-cloudinit-image.sh" /opt/cm4-provisioning/ chmod +x /opt/cm4-provisioning/build-cloudinit-image.sh +cp "$DEPLOY/host/run-shrink-on-host.sh" /opt/cm4-provisioning/ +chmod +x /opt/cm4-provisioning/run-shrink-on-host.sh cp "$DEPLOY/host/cm4-flash-trigger.sh" /usr/local/bin/ chmod +x /usr/local/bin/cm4-flash-trigger.sh cp "$DEPLOY/host/cm4-flash.service" /etc/systemd/system/ cp "$DEPLOY/host/cm4-build-cloudinit.path" /etc/systemd/system/ cp "$DEPLOY/host/cm4-build-cloudinit.service" /etc/systemd/system/ +cp "$DEPLOY/host/cm4-shrink.path" /etc/systemd/system/ +cp "$DEPLOY/host/cm4-shrink.service" /etc/systemd/system/ systemctl daemon-reload systemctl enable --now cm4-build-cloudinit.path 2>/dev/null || true +systemctl enable --now cm4-shrink.path 2>/dev/null || true cp "$DEPLOY/host/89-cm4-boot-mode-permissions.rules" /etc/udev/rules.d/ 2>/dev/null || true cp "$DEPLOY/host/90-cm4-boot-mode.rules" /etc/udev/rules.d/ udevadm control --reload-rules diff --git a/chromium-setup/emmc-provisioning/scripts/install-pishrink-on-host.sh b/chromium-setup/emmc-provisioning/scripts/install-pishrink-on-host.sh index a1b2968..69bf640 100644 --- a/chromium-setup/emmc-provisioning/scripts/install-pishrink-on-host.sh +++ b/chromium-setup/emmc-provisioning/scripts/install-pishrink-on-host.sh @@ -2,19 +2,53 @@ # Run on the provisioning HOST (root) to install PiShrink and dependencies. # Enables shrinking backups in flash-emmc-on-connect.sh when SHRINK_BACKUP=1. # PiShrink: https://github.com/Drewsif/PiShrink +# +# Offline install (when host has no internet): +# 1. On a machine with internet: curl -Lo pishrink.sh https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh +# 2. scp pishrink.sh root@HOST:/tmp/ +# 3. On host: bash install-pishrink-on-host.sh /tmp/pishrink.sh +# +# Optional: PISHRINK_URL=... or pass path to local pishrink.sh as first argument. set -e PISHRINK_URL="${PISHRINK_URL:-https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh}" +LOCAL_PISHRINK="$1" -echo "Installing PiShrink dependencies..." -apt-get update -apt-get install -y wget parted gzip pigz xz-utils udev e2fsprogs +echo "Installing PiShrink dependencies (parted, e2fsprogs, xz-utils, ...)..." +if apt-get update 2>/dev/null && apt-get install -y wget parted gzip pigz xz-utils udev e2fsprogs 2>/dev/null; then + echo "Dependencies installed." +else + echo "Warning: apt install failed (e.g. host has no internet). Skipping packages." + echo "PiShrink needs: parted, e2fsprogs; optional: gzip/pigz, xz-utils. Install them manually if missing." + for cmd in parted resize2fs; do + if ! command -v "$cmd" &>/dev/null; then + echo " Missing: $cmd — install with apt when the host has network, or use a local mirror." + fi + done +fi -echo "Downloading PiShrink..." -wget -q -O /usr/local/bin/pishrink.sh "$PISHRINK_URL" -chmod +x /usr/local/bin/pishrink.sh +if [[ -n "$LOCAL_PISHRINK" && -f "$LOCAL_PISHRINK" ]]; then + echo "Using local PiShrink script: $LOCAL_PISHRINK" + cp "$LOCAL_PISHRINK" /usr/local/bin/pishrink.sh + chmod +x /usr/local/bin/pishrink.sh + echo "PiShrink installed at /usr/local/bin/pishrink.sh" +elif command -v wget &>/dev/null && wget -q -O /usr/local/bin/pishrink.sh "$PISHRINK_URL" 2>/dev/null; then + chmod +x /usr/local/bin/pishrink.sh + echo "PiShrink installed at /usr/local/bin/pishrink.sh" +else + echo "Could not download PiShrink (no wget or no network)." + echo "" + echo "Offline install:" + echo " 1. On a machine WITH internet run:" + echo " curl -Lo pishrink.sh $PISHRINK_URL" + echo " 2. Copy to host:" + echo " scp pishrink.sh root@YOUR_HOST:/tmp/" + echo " 3. On the host run:" + echo " bash install-pishrink-on-host.sh /tmp/pishrink.sh" + exit 1 +fi -echo "PiShrink installed at /usr/local/bin/pishrink.sh" +echo "" echo "To shrink backups automatically, add to /opt/cm4-provisioning/env:" echo " SHRINK_BACKUP=1" echo " # optional: PISHRINK_COMPRESS=gz or PISHRINK_COMPRESS=xz" diff --git a/chromium-setup/start-chromium.sh b/chromium-setup/start-chromium.sh index 04cf078..f455d5c 100755 --- a/chromium-setup/start-chromium.sh +++ b/chromium-setup/start-chromium.sh @@ -39,6 +39,8 @@ for i in {1..10}; do sleep 0.5 done + + # Keep script running wait