Compare commits
2 Commits
ccdace36bc
...
5ff46e67d8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ff46e67d8 | ||
|
|
f93d224e8b |
@@ -0,0 +1,21 @@
|
|||||||
|
#cloud-config
|
||||||
|
# Example user-data that downloads and runs a script from a file server.
|
||||||
|
# Use this so you can change first-boot behaviour without rebuilding the golden image.
|
||||||
|
# Copy to boot partition as 'user-data' and set BOOTSTRAP_URL to your script URL.
|
||||||
|
|
||||||
|
package_update: true
|
||||||
|
package_upgrade: false
|
||||||
|
|
||||||
|
packages:
|
||||||
|
- curl # or wget if you prefer
|
||||||
|
|
||||||
|
# Download script from file server and run it (runcmd runs after network is up)
|
||||||
|
runcmd:
|
||||||
|
- curl -fsSL "http://YOUR_FILE_SERVER/provisioning/bootstrap.sh" -o /tmp/bootstrap.sh
|
||||||
|
- chmod +x /tmp/bootstrap.sh
|
||||||
|
- /tmp/bootstrap.sh
|
||||||
|
# Optional: remove after run
|
||||||
|
# - rm -f /tmp/bootstrap.sh
|
||||||
|
|
||||||
|
# Optional: finish cloud-init
|
||||||
|
# - cloud-init single --name cc_final_message
|
||||||
@@ -8,6 +8,7 @@ Supports USB boot mode (ask Backup/Deploy) and network-booted devices (register,
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -114,7 +115,7 @@ def _safe_backup_name(name):
|
|||||||
"""Reject path traversal and ensure it's a backup filename we manage."""
|
"""Reject path traversal and ensure it's a backup filename we manage."""
|
||||||
if not name or ".." in name or "/" in name or "\\" in name:
|
if not name or ".." in name or "/" in name or "\\" in name:
|
||||||
return False
|
return False
|
||||||
if not (name.endswith(".img") or name.endswith(".img.gz")):
|
if not name.endswith((".img", ".img.gz", ".img.xz")):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -125,7 +126,7 @@ def list_backups():
|
|||||||
meta = _load_backups_meta()
|
meta = _load_backups_meta()
|
||||||
out = []
|
out = []
|
||||||
for p in sorted(BACKUPS_DIR.iterdir(), key=lambda x: x.stat().st_mtime, reverse=True):
|
for p in sorted(BACKUPS_DIR.iterdir(), key=lambda x: x.stat().st_mtime, reverse=True):
|
||||||
if p.is_file() and p.suffix in (".img", ".img.gz") and p.name != "backups_meta.json":
|
if p.is_file() and p.name != "backups_meta.json" and p.name.endswith((".img", ".img.gz", ".img.xz")):
|
||||||
try:
|
try:
|
||||||
st = p.stat()
|
st = p.stat()
|
||||||
m = meta.get(p.name, {})
|
m = meta.get(p.name, {})
|
||||||
@@ -195,6 +196,12 @@ def api_device_action():
|
|||||||
if source == "usb":
|
if source == "usb":
|
||||||
try:
|
try:
|
||||||
os.makedirs(os.path.dirname(ACTION_REQUEST_FILE) or ".", exist_ok=True)
|
os.makedirs(os.path.dirname(ACTION_REQUEST_FILE) or ".", exist_ok=True)
|
||||||
|
# If user requested "shrink after backup", create flag so host runs PiShrink after dd
|
||||||
|
if action == "backup" and body.get("shrink"):
|
||||||
|
try:
|
||||||
|
(BASE_DIR / "shrink_next_backup").write_text("1")
|
||||||
|
except (PermissionError, OSError):
|
||||||
|
pass # host may still have SHRINK_BACKUP=1
|
||||||
with open(ACTION_REQUEST_FILE, "w") as f:
|
with open(ACTION_REQUEST_FILE, "w") as f:
|
||||||
f.write(action)
|
f.write(action)
|
||||||
return jsonify({"ok": True})
|
return jsonify({"ok": True})
|
||||||
@@ -314,6 +321,40 @@ def api_backup_set_as_golden(name):
|
|||||||
return jsonify({"ok": False, "error": str(e)}), 500
|
return jsonify({"ok": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/backups/<path:name>/shrink", methods=["POST"])
|
||||||
|
def api_backup_shrink(name):
|
||||||
|
"""Run PiShrink on a raw .img backup (shrinks in place). Requires PiShrink in LXC/host."""
|
||||||
|
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"):
|
||||||
|
return jsonify({"ok": False, "error": "only raw .img files can be shrunk (not .img.gz / .img.xz)"}), 400
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/backups/<path:name>", methods=["PATCH"])
|
@app.route("/api/backups/<path:name>", methods=["PATCH"])
|
||||||
def api_backup_update(name):
|
def api_backup_update(name):
|
||||||
"""Update backup metadata (display name, description) or rename the file."""
|
"""Update backup metadata (display name, description) or rename the file."""
|
||||||
|
|||||||
@@ -382,6 +382,9 @@
|
|||||||
<section class="section">
|
<section class="section">
|
||||||
<h2 class="section-title">Capture image or deploy</h2>
|
<h2 class="section-title">Capture image or deploy</h2>
|
||||||
<p class="backup-deploy-hint">To <strong>capture (backup)</strong> an image from a device: connect it in USB boot mode or register over network. When the device appears below, click <strong>Backup</strong> to save its eMMC to a file. To write an image to a device, click <strong>Deploy</strong> (requires a golden image).</p>
|
<p class="backup-deploy-hint">To <strong>capture (backup)</strong> an image from a device: connect it in USB boot mode or register over network. When the device appears below, click <strong>Backup</strong> to save its eMMC to a file. To write an image to a device, click <strong>Deploy</strong> (requires a golden image).</p>
|
||||||
|
<p id="shrinkOptionWrap" class="backup-deploy-hint" style="display:none; margin-top:0.5rem;">
|
||||||
|
<label><input type="checkbox" id="shrinkAfterBackup" /> Shrink after backup</label> <span class="backups-mono" style="font-size:0.8rem;">(reduces size; requires PiShrink on host)</span>
|
||||||
|
</p>
|
||||||
<div id="pendingDevices"></div>
|
<div id="pendingDevices"></div>
|
||||||
<div id="noPendingPlaceholder" class="placeholder-actions" style="display:none;">
|
<div id="noPendingPlaceholder" class="placeholder-actions" style="display:none;">
|
||||||
<span>Connect a device to see:</span>
|
<span>Connect a device to see:</span>
|
||||||
@@ -506,12 +509,16 @@
|
|||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
let hasAny = false;
|
let hasAny = false;
|
||||||
|
|
||||||
|
const shrinkWrap = document.getElementById('shrinkOptionWrap');
|
||||||
if (usb) {
|
if (usb) {
|
||||||
hasAny = true;
|
hasAny = true;
|
||||||
|
if (shrinkWrap) shrinkWrap.style.display = 'block';
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'device-item';
|
el.className = 'device-item';
|
||||||
el.innerHTML = '<div class="device-info"><div class="device-type">USB boot</div><div class="device-desc">Device connected — choose Backup or Deploy</div></div><div class="device-actions"><button type="button" class="btn btn-outline" data-source="usb" data-action="backup">Backup</button><button type="button" class="btn btn-primary" data-source="usb" data-action="deploy">Deploy</button></div>';
|
el.innerHTML = '<div class="device-info"><div class="device-type">USB boot</div><div class="device-desc">Device connected — choose Backup or Deploy</div></div><div class="device-actions"><button type="button" class="btn btn-outline" data-source="usb" data-action="backup">Backup</button><button type="button" class="btn btn-primary" data-source="usb" data-action="deploy">Deploy</button></div>';
|
||||||
container.appendChild(el);
|
container.appendChild(el);
|
||||||
|
} else {
|
||||||
|
if (shrinkWrap) shrinkWrap.style.display = 'none';
|
||||||
}
|
}
|
||||||
(network || []).forEach(function(d) {
|
(network || []).forEach(function(d) {
|
||||||
hasAny = true;
|
hasAny = true;
|
||||||
@@ -532,6 +539,8 @@
|
|||||||
const mac = btn.getAttribute('data-mac');
|
const mac = btn.getAttribute('data-mac');
|
||||||
const body = { source: source, action: action };
|
const body = { source: source, action: action };
|
||||||
if (mac) body.mac = mac;
|
if (mac) body.mac = mac;
|
||||||
|
const shrinkCb = document.getElementById('shrinkAfterBackup');
|
||||||
|
if (action === 'backup' && shrinkCb && shrinkCb.checked) body.shrink = true;
|
||||||
fetch('/api/device-action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
fetch('/api/device-action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
@@ -557,12 +566,15 @@
|
|||||||
tr.dataset.name = b.name;
|
tr.dataset.name = b.name;
|
||||||
const displayName = (b.display_name || b.name);
|
const displayName = (b.display_name || b.name);
|
||||||
const desc = (b.description || '');
|
const desc = (b.description || '');
|
||||||
|
const isRawImg = b.name.endsWith('.img') && !b.name.endsWith('.img.gz') && !b.name.endsWith('.img.xz');
|
||||||
|
const shrinkBtn = isRawImg ? '<button type="button" class="btn btn-outline btn-sm shrink-btn" data-name="' + escapeHtml(b.name) + '" title="Shrink image (PiShrink)">Shrink</button> ' : '';
|
||||||
tr.innerHTML =
|
tr.innerHTML =
|
||||||
'<td class="backup-name-edit" data-field="name" title="Click to rename">' + escapeHtml(displayName) + '</td>' +
|
'<td class="backup-name-edit" data-field="name" title="Click to rename">' + escapeHtml(displayName) + '</td>' +
|
||||||
'<td class="backup-desc-edit" data-field="description" title="Click to add description">' + escapeHtml(desc) + '</td>' +
|
'<td class="backup-desc-edit" data-field="description" title="Click to add description">' + escapeHtml(desc) + '</td>' +
|
||||||
'<td class="backups-mono">' + fmtSize(b.size) + '</td>' +
|
'<td class="backups-mono">' + fmtSize(b.size) + '</td>' +
|
||||||
'<td class="backups-mono">' + fmtDate(b.mtime) + '</td>' +
|
'<td class="backups-mono">' + fmtDate(b.mtime) + '</td>' +
|
||||||
'<td class="actions-cell">' +
|
'<td class="actions-cell">' +
|
||||||
|
shrinkBtn +
|
||||||
'<button type="button" class="btn btn-primary btn-sm set-golden-btn" data-name="' + escapeHtml(b.name) + '">Set as golden</button> ' +
|
'<button type="button" class="btn btn-primary btn-sm set-golden-btn" data-name="' + escapeHtml(b.name) + '">Set as golden</button> ' +
|
||||||
'<button type="button" class="btn btn-outline btn-sm rename-file-btn" data-name="' + escapeHtml(b.name) + '" title="Rename file">Rename file</button> ' +
|
'<button type="button" class="btn btn-outline btn-sm rename-file-btn" data-name="' + escapeHtml(b.name) + '" title="Rename file">Rename file</button> ' +
|
||||||
'<a href="/api/backups/' + encodeURIComponent(b.name) + '" download class="btn btn-outline btn-sm download-link">Download</a>' +
|
'<a href="/api/backups/' + encodeURIComponent(b.name) + '" download class="btn btn-outline btn-sm download-link">Download</a>' +
|
||||||
@@ -572,6 +584,7 @@
|
|||||||
bindBackupEdits();
|
bindBackupEdits();
|
||||||
bindSetGolden();
|
bindSetGolden();
|
||||||
bindRenameFile();
|
bindRenameFile();
|
||||||
|
bindShrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindRenameFile() {
|
function bindRenameFile() {
|
||||||
@@ -648,6 +661,25 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bindShrink() {
|
||||||
|
document.querySelectorAll('.shrink-btn').forEach(function(btn) {
|
||||||
|
btn.onclick = function() {
|
||||||
|
const name = btn.getAttribute('data-name');
|
||||||
|
if (!confirm('Shrink this image with PiShrink? This reduces file size and may take a few minutes.\n\n' + name)) return;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Shrinking…';
|
||||||
|
fetch('/api/backups/' + encodeURIComponent(name) + '/shrink', { method: 'POST' })
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.ok) { fetchBackups(); }
|
||||||
|
else alert(data.error || data.detail || 'Failed');
|
||||||
|
})
|
||||||
|
.catch(function() { alert('Request failed'); })
|
||||||
|
.finally(function() { btn.disabled = false; btn.textContent = 'Shrink'; });
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function fetchGoldenInfo() {
|
function fetchGoldenInfo() {
|
||||||
fetch('/api/golden-info').then(function(r) { return r.json(); }).then(function(d) {
|
fetch('/api/golden-info').then(function(r) { return r.json(); }).then(function(d) {
|
||||||
const el = document.getElementById('goldenHint');
|
const el = document.getElementById('goldenHint');
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ sudo cp rpiboot /opt/usbboot/
|
|||||||
sudo cp /path/to/your/golden-reterminal.img /var/lib/cm4-provisioning/golden.img
|
sudo cp /path/to/your/golden-reterminal.img /var/lib/cm4-provisioning/golden.img
|
||||||
```
|
```
|
||||||
|
|
||||||
- Or use a different path and set `GOLDEN_IMAGE` when installing the script (see below).
|
- Or use a different path and set `GOLDEN_IMAGE` when installing the script (see below). On Proxmox with LXC, you can store backup images on a host directory by setting `CM4_BACKUPS_HOST_PATH` at deploy so that folder is bind-mounted into the LXC — see **PROXMOX-LXC-DEPLOYMENT.md** § Store backup images on a host directory.
|
||||||
|
|
||||||
#### 3. Install the provisioning script and trigger
|
#### 3. Install the provisioning script and trigger
|
||||||
|
|
||||||
@@ -59,6 +59,9 @@ sudo chmod +x /opt/cm4-provisioning/flash-emmc-on-connect.sh
|
|||||||
echo 'GOLDEN_IMAGE=/var/lib/cm4-provisioning/golden.img' | sudo tee /opt/cm4-provisioning/env
|
echo 'GOLDEN_IMAGE=/var/lib/cm4-provisioning/golden.img' | sudo tee /opt/cm4-provisioning/env
|
||||||
echo 'RPIBOOT_DIR=/opt/usbboot' | sudo tee -a /opt/cm4-provisioning/env
|
echo 'RPIBOOT_DIR=/opt/usbboot' | sudo tee -a /opt/cm4-provisioning/env
|
||||||
echo 'EMMC_SIZE_BYTES=8589934592' | sudo tee -a /opt/cm4-provisioning/env # 8GB; use 17179869184 for 16GB
|
echo 'EMMC_SIZE_BYTES=8589934592' | sudo tee -a /opt/cm4-provisioning/env # 8GB; use 17179869184 for 16GB
|
||||||
|
# Optional: shrink backups after dd (requires PiShrink; see "Shrinking backup and golden images" below)
|
||||||
|
# echo 'SHRINK_BACKUP=1' | sudo tee -a /opt/cm4-provisioning/env
|
||||||
|
# echo 'PISHRINK_COMPRESS=gz' | sudo tee -a /opt/cm4-provisioning/env # or xz
|
||||||
|
|
||||||
sudo cp cm4-flash-trigger.sh /usr/local/bin/
|
sudo cp cm4-flash-trigger.sh /usr/local/bin/
|
||||||
sudo chmod +x /usr/local/bin/cm4-flash-trigger.sh
|
sudo chmod +x /usr/local/bin/cm4-flash-trigger.sh
|
||||||
@@ -116,6 +119,53 @@ Or keep it simple and edit the defaults inside `flash-emmc-on-connect.sh` (e.g.
|
|||||||
|
|
||||||
Raspberry Pi OS (recent versions) supports **cloud-init** using the **NoCloud** datasource: it reads `user-data`, `meta-data`, and optionally `network-config` from the **boot** (FAT32) partition.
|
Raspberry Pi OS (recent versions) supports **cloud-init** using the **NoCloud** datasource: it reads `user-data`, `meta-data`, and optionally `network-config` from the **boot** (FAT32) partition.
|
||||||
|
|
||||||
|
### Steps to prepare a cloud-init image for Raspberry Pi OS
|
||||||
|
|
||||||
|
1. **Obtain Raspberry Pi OS**
|
||||||
|
- Download from [raspberrypi.com/software](https://www.raspberrypi.com/software/) (desktop or Lite), or use **Raspberry Pi Imager** with "Edit settings" for locale/SSH. Recent Raspberry Pi OS has cloud-init built in.
|
||||||
|
|
||||||
|
2. **Flash the image**
|
||||||
|
- Flash to a spare SD card, or to a loop file for building without physical media: `cp raspios.img golden-work.img`
|
||||||
|
|
||||||
|
3. **Mount the boot partition**
|
||||||
|
- **From an image file**: `sudo losetup -fP golden-work.img` then e.g. `sudo mount /dev/loop0p1 /mnt/boot`
|
||||||
|
- **From SD**: mount the first (FAT32) partition at `/mnt/boot`. On a running Pi, boot is often `/boot/firmware`.
|
||||||
|
|
||||||
|
4. **Add NoCloud files on the boot partition** (root of FAT32, same level as `config.txt`):
|
||||||
|
- `user-data`, `meta-data`, `network-config`
|
||||||
|
- From this repo: `cp emmc-provisioning/cloud-init/{user-data,meta-data,network-config} /mnt/boot/`
|
||||||
|
|
||||||
|
5. **Customise** `user-data` and `network-config`. Use the **remote bootstrap script** pattern (below) to avoid rebuilding the image when you change first-boot commands.
|
||||||
|
|
||||||
|
6. **Unmount and create golden image**: `sudo umount /mnt/boot`, then copy the image to your `GOLDEN_IMAGE` path (e.g. `/var/lib/cm4-provisioning/golden.img`).
|
||||||
|
|
||||||
|
### Remote bootstrap script (no image rebuild for script changes)
|
||||||
|
|
||||||
|
You can keep the golden image **fixed** and have cloud-init **download a script from a file server** and run it on first boot. When you change the script on the server, the next device gets the new commands without rebuilding the image.
|
||||||
|
|
||||||
|
1. **Host the script** on an HTTP/HTTPS file server (e.g. nginx, or `python3 -m http.server`) at a URL the Pi can reach, e.g. `http://192.168.1.10/provisioning/bootstrap.sh`.
|
||||||
|
|
||||||
|
2. **In `user-data`, use `runcmd` to download and run it** (runcmd runs after packages and network are up):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
#cloud-config
|
||||||
|
package_update: true
|
||||||
|
package_upgrade: false
|
||||||
|
packages:
|
||||||
|
- curl # or wget
|
||||||
|
|
||||||
|
runcmd:
|
||||||
|
- curl -fsSL "http://YOUR_FILE_SERVER/provisioning/bootstrap.sh" -o /tmp/bootstrap.sh
|
||||||
|
- chmod +x /tmp/bootstrap.sh
|
||||||
|
- /tmp/bootstrap.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
With **wget**: `wget -q -O /tmp/bootstrap.sh "http://.../bootstrap.sh"` then `chmod +x /tmp/bootstrap.sh` and `/tmp/bootstrap.sh`.
|
||||||
|
|
||||||
|
3. **Bootstrap script**: use a normal shell script (e.g. `#!/bin/bash` and `set -e`). It can install packages, configure kiosk, set hostname, register with a dashboard, etc.
|
||||||
|
|
||||||
|
4. **Notes**: Ensure `network-config` (or DHCP) gives the Pi an IP before runcmd. For HTTPS, add `ca-certificates` to `packages` if needed. You can use one script for all devices or have the script call your server with serial/MAC for device-specific config.
|
||||||
|
|
||||||
### Creating the golden image
|
### Creating the golden image
|
||||||
|
|
||||||
1. **Flash Raspberry Pi OS** (or your base image) to a spare SD card or a loop file.
|
1. **Flash Raspberry Pi OS** (or your base image) to a spare SD card or a loop file.
|
||||||
@@ -135,9 +185,47 @@ cp emmc-provisioning/cloud-init/meta-data /mnt/boot/
|
|||||||
cp emmc-provisioning/cloud-init/network-config /mnt/boot/
|
cp emmc-provisioning/cloud-init/network-config /mnt/boot/
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Customise** `user-data` and `network-config` (hostname, WiFi, packages, Chromium kiosk, etc.).
|
4. **Customise** `user-data` and `network-config` (hostname, WiFi, packages, Chromium kiosk, or a single runcmd that downloads and runs a remote script—see "Remote bootstrap script" above).
|
||||||
5. **Copy your kiosk/Chromium scripts** into the image rootfs if needed (e.g. under `/home/pi/` or `/opt/`) and reference them from `user-data` `runcmd` or a systemd unit.
|
5. **Copy your kiosk/Chromium scripts** into the image rootfs only if you are not using a remote script; otherwise the remote script can pull what it needs.
|
||||||
6. **Unmount**, then create a **golden image** from the SD or loop device (e.g. `dd` or `dd` of the whole block device). Use that as `golden.img` on the provisioning host.
|
6. **Unmount**, then create a **golden image** from the SD or loop device (e.g. `dd` or copy of the whole block device). Use that as `golden.img` on the provisioning host. Optionally shrink it with PiShrink (see below) to save space and speed up deploy.
|
||||||
|
|
||||||
|
### Shrinking backup and golden images (PiShrink)
|
||||||
|
|
||||||
|
Raw full-disk backups and golden images are the full size of the eMMC (e.g. 32 GB). [PiShrink](https://github.com/Drewsif/PiShrink) shrinks the **last partition** (must be ext2/3/4) to its minimum size and truncates the image file; on first boot the rootfs can expand back to fill the device. This reduces backup/golden image size (often to a few GB) and improves transfer times.
|
||||||
|
|
||||||
|
**On the provisioning host:**
|
||||||
|
|
||||||
|
1. **Install PiShrink and dependencies** (run as root on the host, or via `ssh root@HOST 'bash -s' < scripts/install-pishrink-on-host.sh`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From this repo (on the host or via ssh)
|
||||||
|
bash chromium-setup/emmc-provisioning/scripts/install-pishrink-on-host.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs `parted`, `e2fsprogs`, `gzip`, `pigz`, `xz-utils` and downloads `pishrink.sh` to `/usr/local/bin/pishrink.sh`.
|
||||||
|
|
||||||
|
2. **Shrink backups automatically** after each Backup in the dashboard: add to `/opt/cm4-provisioning/env` and ensure the trigger sources it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo 'SHRINK_BACKUP=1' | sudo tee -a /opt/cm4-provisioning/env
|
||||||
|
# Optional: compress after shrinking (smaller file, but must decompress before using as golden image)
|
||||||
|
# echo 'PISHRINK_COMPRESS=gz' | sudo tee -a /opt/cm4-provisioning/env # or xz
|
||||||
|
```
|
||||||
|
|
||||||
|
With `SHRINK_BACKUP=1`, once a backup finishes, the script runs PiShrink on the `.img` file. Use `PISHRINK_COMPRESS=gz` or `xz` for maximum size reduction; the file becomes `.img.gz` or `.img.xz` and must be decompressed before deploy (e.g. `gunzip -c backup.img.gz > golden.img`).
|
||||||
|
|
||||||
|
3. **Shrink a golden image manually** (e.g. after building from Raspberry Pi OS):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo pishrink.sh -n /path/to/large.img /var/lib/cm4-provisioning/golden.img
|
||||||
|
```
|
||||||
|
|
||||||
|
`-n` disables update check. Omit the second argument to shrink in place. The shrunk image will expand the rootfs on first boot when deployed to eMMC.
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
|
||||||
|
- 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`).
|
||||||
|
|
||||||
### Cloud-init file locations on the Pi
|
### Cloud-init file locations on the Pi
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,19 @@ No root password is set by default. To log in as root over SSH:
|
|||||||
- **Option B – Manual:**
|
- **Option B – Manual:**
|
||||||
`ssh root@10.130.60.224` then `pct exec 201 -- bash` to get a shell in the container. Run `apt-get install -y openssh-server`, edit `/etc/ssh/sshd_config` to set `PermitRootLogin yes`, run `passwd` to set root password, add your key to `/root/.ssh/authorized_keys`, and restart `ssh`.
|
`ssh root@10.130.60.224` then `pct exec 201 -- bash` to get a shell in the container. Run `apt-get install -y openssh-server`, edit `/etc/ssh/sshd_config` to set `PermitRootLogin yes`, run `passwd` to set root password, add your key to `/root/.ssh/authorized_keys`, and restart `ssh`.
|
||||||
|
|
||||||
### 3. Put the golden image on the host (or in the LXC)
|
### 3. (Optional) Store backup images on a host directory
|
||||||
|
|
||||||
|
To keep backup images on a specific host path (e.g. a large disk or NFS mount) instead of under `/var/lib/cm4-provisioning/backups`, deploy with **`CM4_BACKUPS_HOST_PATH`** set. That directory is created on the host, bind-mounted into the LXC at `/var/lib/cm4-provisioning/backups`, and the host flash script is configured to write backups there. The dashboard in the LXC then lists and serves those same files.
|
||||||
|
|
||||||
|
**Deploy with a host backup path:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CM4_BACKUPS_HOST_PATH=/mnt/storage/cm4-backups ./chromium-setup/emmc-provisioning/scripts/deploy-to-proxmox.sh root@10.130.60.224
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `/mnt/storage/cm4-backups` (or your path) on the host first if it doesn’t exist; the deploy script will create it if possible. To add or change the backup mount on an already-deployed host, set `CM4_BACKUPS_HOST_PATH` and run the deploy script again, then on the host add `BACKUPS_DIR=<path>` to `/opt/cm4-provisioning/env` and add the bind mount (see deploy script for the `pct set 201 -mp1 ...` step).
|
||||||
|
|
||||||
|
### 4. Put the golden image on the host (or in the LXC)
|
||||||
|
|
||||||
The image must be at **`/var/lib/cm4-provisioning/golden.img`** on the **host**. Because that directory is bind-mounted into the LXC, you can use either:
|
The image must be at **`/var/lib/cm4-provisioning/golden.img`** on the **host**. Because that directory is bind-mounted into the LXC, you can use either:
|
||||||
|
|
||||||
@@ -92,7 +104,7 @@ The image must be at **`/var/lib/cm4-provisioning/golden.img`** on the **host**.
|
|||||||
# Copy to that path inside the container; it's the same as the host path.
|
# Copy to that path inside the container; it's the same as the host path.
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Run the provisioning dashboard (optional)
|
### 5. Run the provisioning dashboard (optional)
|
||||||
|
|
||||||
The dashboard shows **connection steps** and **live deployment status** (idle / connecting / flashing / done / error) and a recent flash log. It reads the same `status.json` and `flash.log` that the host’s flash script writes (via the bind-mounted `/var/lib/cm4-provisioning`).
|
The dashboard shows **connection steps** and **live deployment status** (idle / connecting / flashing / done / error) and a recent flash log. It reads the same `status.json` and `flash.log` that the host’s flash script writes (via the bind-mounted `/var/lib/cm4-provisioning`).
|
||||||
|
|
||||||
@@ -116,7 +128,7 @@ systemctl enable --now cm4-dashboard
|
|||||||
|
|
||||||
Then open **http://<LXC-201-IP>:5000** (get the IP with `pct exec 201 -- hostname -I`). If the LXC is on a private network, set up port forwarding on the Proxmox host or use a reverse proxy so you can reach the dashboard from your browser.
|
Then open **http://<LXC-201-IP>:5000** (get the IP with `pct exec 201 -- hostname -I`). If the LXC is on a private network, set up port forwarding on the Proxmox host or use a reverse proxy so you can reach the dashboard from your browser.
|
||||||
|
|
||||||
### 5. Optional: disable or enable auto-flash
|
### 6. Optional: disable or enable auto-flash
|
||||||
|
|
||||||
- **Disable:**
|
- **Disable:**
|
||||||
`ssh root@10.130.60.224 "rm /etc/cm4-provisioning/enabled"`
|
`ssh root@10.130.60.224 "rm /etc/cm4-provisioning/enabled"`
|
||||||
@@ -153,7 +165,7 @@ From the **Proxmox host** you can monitor:
|
|||||||
| **Flash job** | `systemctl status cm4-flash-once` — whether the udev-triggered job is running/failed |
|
| **Flash job** | `systemctl status cm4-flash-once` — whether the udev-triggered job is running/failed |
|
||||||
| **Journal** | `journalctl -u cm4-flash-once -f` or `journalctl -t cm4-flash -f` — systemd/log output |
|
| **Journal** | `journalctl -u cm4-flash-once -f` or `journalctl -t cm4-flash -f` — systemd/log output |
|
||||||
| **Block devices** | `lsblk` — after rpiboot, the eMMC appears as a new disk (e.g. `/dev/sdb`) |
|
| **Block devices** | `lsblk` — after rpiboot, the eMMC appears as a new disk (e.g. `/dev/sdb`) |
|
||||||
| **Backups** | `ls /var/lib/cm4-provisioning/backups/` — backup images created from the dashboard |
|
| **Backups** | `ls /var/lib/cm4-provisioning/backups/` — backup images (on host; if you used `CM4_BACKUPS_HOST_PATH` they are under that path on the host, bind-mounted into the LXC). To shrink automatically, set `SHRINK_BACKUP=1` in `/opt/cm4-provisioning/env` — see **EMMC-PROVISIONING-GUIDE.md** § Shrinking backup and golden images. |
|
||||||
| **Config** | `cat /opt/cm4-provisioning/env` — GOLDEN_IMAGE, RPIBOOT_DIR, EMMC_SIZE_BYTES |
|
| **Config** | `cat /opt/cm4-provisioning/env` — GOLDEN_IMAGE, RPIBOOT_DIR, EMMC_SIZE_BYTES |
|
||||||
|
|
||||||
**One-command snapshot:**
|
**One-command snapshot:**
|
||||||
@@ -167,6 +179,28 @@ Or copy `scripts/monitor-from-host.sh` to the host and run `./monitor-from-host.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Troubleshooting: device connected but not shown in portal
|
||||||
|
|
||||||
|
1. **Host has old flash script** – The script must *not* exit when the golden image is missing (so you can use Backup first). Update the host:
|
||||||
|
```bash
|
||||||
|
scp chromium-setup/emmc-provisioning/host/flash-emmc-on-connect.sh root@10.130.60.224:/opt/cm4-provisioning/
|
||||||
|
ssh root@10.130.60.224 "chmod +x /opt/cm4-provisioning/flash-emmc-on-connect.sh"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Unplug and replug the USB** – udev runs the trigger only when the device is *added*. Unplug the reTerminal USB (keep it in boot mode), then plug it back in. The trigger will run the script and rpiboot; when the eMMC is exposed, the portal shows "Device connected" with Backup/Deploy.
|
||||||
|
|
||||||
|
3. **If rpiboot fails** – Check on the host: `ssh root@10.130.60.224 'tail -30 /var/lib/cm4-provisioning/flash.log'` (rpiboot stderr is appended there). Try unplug/replug again. To see the exact rpiboot error: `ssh root@10.130.60.224 '/opt/usbboot/rpiboot -d /opt/usbboot/mass-storage-gadget64'` (device connected; Ctrl+C to stop). Run `scripts/monitor-from-host.sh` for a full snapshot.
|
||||||
|
|
||||||
|
4. **"No 'bootcode' files found in mass-storage-gadget64"** – Usually because `bootfiles.bin` is a **broken symlink** (e.g. `-> ../firmware/bootfiles.bin`) and that target doesn’t exist. **Fix on host:** run `scripts/fix-gadget-bootcode-on-host.sh` on the host (it removes the symlink and extracts `bootcode4.bin` from the installed rpiboot binary). From your machine: `ssh root@10.130.60.224 'bash -s' < scripts/fix-gadget-bootcode-on-host.sh`. **Alternative:** repopulate the gadget dir with `./scripts/populate-gadget-on-host.sh root@10.130.60.224`, or full reinstall with `./scripts/build-and-deploy-usbboot-to-host.sh root@10.130.60.224`. Then verify: `ls -la /opt/usbboot/mass-storage-gadget64/` (should list a real `bootcode4.bin` or `bootfiles.bin`, plus `boot.img`, `config.txt`).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Redeploy / update scripts
|
## Redeploy / update scripts
|
||||||
|
|
||||||
From your repo (e.g. after changing scripts):
|
From your repo (e.g. after changing scripts):
|
||||||
@@ -188,5 +222,5 @@ That script syncs the repo to the host and reinstalls scripts on both the host a
|
|||||||
| Flash runs on | Proxmox **host** (udev + rpiboot + dd) |
|
| Flash runs on | Proxmox **host** (udev + rpiboot + dd) |
|
||||||
| Build rpiboot on host | Run `scripts/install-usbboot-on-host.sh` on the host when it has internet |
|
| Build rpiboot on host | Run `scripts/install-usbboot-on-host.sh` on the host when it has internet |
|
||||||
| Dashboard | Flask app in LXC at `http://<LXC-IP>:5000`; switch Flash/Backup mode, list and download backups; see **dashboard/README.md** and section 3 above |
|
| Dashboard | Flask app in LXC at `http://<LXC-IP>:5000`; switch Flash/Backup mode, list and download backups; see **dashboard/README.md** and section 3 above |
|
||||||
| Backups | Saved under `/var/lib/cm4-provisioning/backups/`. When a device is detected (USB or network), choose **Backup** or **Deploy** in the dashboard. |
|
| Backups | Saved under `/var/lib/cm4-provisioning/backups/` (optionally a host path bind-mounted into the LXC — set `CM4_BACKUPS_HOST_PATH` at deploy). When a device is detected, choose **Backup** or **Deploy** in the dashboard. |
|
||||||
| Network deploy/backup | Network-booted devices run **network-client/provisioning-client.sh** and register with the dashboard; they then appear under "Device detected (Network)" and you choose Backup or Deploy. See **network-client/README.md**. |
|
| Network deploy/backup | Network-booted devices run **network-client/provisioning-client.sh** and register with the dashboard; they then appear under "Device detected (Network)" and you choose Backup or Deploy. See **network-client/README.md**. |
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Ensure CM4 boot-mode USB device is accessible to rpiboot (libusb).
|
||||||
|
# Load before 90-cm4-boot-mode.rules so permissions are set before the trigger runs.
|
||||||
|
SUBSYSTEM=="usb", ATTR{idVendor}=="2b8e", MODE="0666"
|
||||||
|
SUBSYSTEM=="usb", ATTR{idVendor}=="0a5c", ATTR{idProduct}=="2711", MODE="0666"
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Called by udev when CM4 in boot mode is connected. Starts the provisioning script in the
|
# Called by udev when CM4 in boot mode (0a5c:2711 or 2b8e) is connected.
|
||||||
# background (rpiboot + wait for portal Backup/Deploy choice). Install to /usr/local/bin/cm4-flash-trigger.sh
|
# Start the flash service via systemd so it runs under systemd, not udev —
|
||||||
|
# otherwise udev kills the process when the device re-enumerates (first→second stage).
|
||||||
|
# --no-block: return immediately so udev doesn't wait; the service runs in the background.
|
||||||
|
|
||||||
FLASH_SCRIPT="${CM4_FLASH_SCRIPT:-/opt/cm4-provisioning/flash-emmc-on-connect.sh}"
|
systemctl --no-block start cm4-flash.service
|
||||||
exec systemd-run --no-block --unit=cm4-flash-once "$FLASH_SCRIPT"
|
|
||||||
|
|||||||
21
chromium-setup/emmc-provisioning/host/cm4-flash.service
Normal file
21
chromium-setup/emmc-provisioning/host/cm4-flash.service
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=CM4 eMMC provisioning (rpiboot + backup/deploy)
|
||||||
|
# Run after udev has settled; do not block boot
|
||||||
|
After=systemd-udevd.service
|
||||||
|
DefaultDependencies=yes
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
# Delay so USB device is enumerated and udev permissions applied before we run
|
||||||
|
ExecStartPre=/bin/sleep 5
|
||||||
|
ExecStart=/opt/cm4-provisioning/flash-emmc-on-connect.sh
|
||||||
|
# Run as root; flash script logs to /var/lib/cm4-provisioning/flash.log
|
||||||
|
User=root
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
# Allow long run: rpiboot + wait for choice + dd 32GB (~20–35 min) + optional PiShrink (~15–30 min)
|
||||||
|
TimeoutStartSec=7200
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
# Only started by udev trigger, not at boot
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -4,7 +4,11 @@
|
|||||||
# to choose Backup or Deploy in the portal — no auto-flash; action runs only after portal choice.
|
# to choose Backup or Deploy in the portal — no auto-flash; action runs only after portal choice.
|
||||||
# Run this from udev or a systemd service. Requires: usbboot (rpiboot) built, golden image for deploy.
|
# Run this from udev or a systemd service. Requires: usbboot (rpiboot) built, golden image for deploy.
|
||||||
|
|
||||||
set -e
|
# Do NOT use set -e: rpiboot and intermediate commands may return non-zero without being fatal
|
||||||
|
|
||||||
|
# Redirect all output to log file (stdout + stderr) so nothing is lost to nohup buffering
|
||||||
|
LOG_FILE="${LOG_FILE:-/var/lib/cm4-provisioning/flash.log}"
|
||||||
|
exec >> "$LOG_FILE" 2>&1
|
||||||
|
|
||||||
# Load overrides from env file if present
|
# Load overrides from env file if present
|
||||||
[[ -f /opt/cm4-provisioning/env ]] && source /opt/cm4-provisioning/env
|
[[ -f /opt/cm4-provisioning/env ]] && source /opt/cm4-provisioning/env
|
||||||
@@ -12,13 +16,14 @@ set -e
|
|||||||
# Configuration - adjust paths and size for your setup
|
# Configuration - adjust paths and size for your setup
|
||||||
RPIBOOT_DIR="${RPIBOOT_DIR:-/opt/usbboot}"
|
RPIBOOT_DIR="${RPIBOOT_DIR:-/opt/usbboot}"
|
||||||
GOLDEN_IMAGE="${GOLDEN_IMAGE:-/var/lib/cm4-provisioning/golden.img}"
|
GOLDEN_IMAGE="${GOLDEN_IMAGE:-/var/lib/cm4-provisioning/golden.img}"
|
||||||
# Expected eMMC size in bytes (reTerminal CM4 often 8GB or 16GB). Used to identify the correct block device.
|
# Expected eMMC size in bytes. reTerminal DM (CM4) has 32 GB eMMC (~31268536320 bytes).
|
||||||
EMMC_SIZE_BYTES="${EMMC_SIZE_BYTES:-$(( 8 * 1024 * 1024 * 1024 ))}"
|
EMMC_SIZE_BYTES="${EMMC_SIZE_BYTES:-$(( 32 * 1024 * 1024 * 1024 ))}"
|
||||||
LOG_TAG="cm4-flash"
|
LOG_TAG="cm4-flash"
|
||||||
STATUS_FILE="${STATUS_FILE:-/var/lib/cm4-provisioning/status.json}"
|
STATUS_FILE="${STATUS_FILE:-/var/lib/cm4-provisioning/status.json}"
|
||||||
LOG_FILE="${LOG_FILE:-/var/lib/cm4-provisioning/flash.log}"
|
LOG_FILE="${LOG_FILE:-/var/lib/cm4-provisioning/flash.log}"
|
||||||
|
LOCK_FILE="${LOCK_FILE:-/var/lib/cm4-provisioning/flash.lock}"
|
||||||
|
|
||||||
log() { echo "[$LOG_TAG] $*"; logger -t "$LOG_TAG" "$*"; echo "[$(date -Iseconds)] $*" >> "$LOG_FILE" 2>/dev/null || true; }
|
log() { echo "[$(date -Iseconds)] [$LOG_TAG] $*"; logger -t "$LOG_TAG" "$*" 2>/dev/null || true; }
|
||||||
|
|
||||||
write_status() {
|
write_status() {
|
||||||
local phase="$1" message="$2" progress="${3:-null}" error="${4:-}"
|
local phase="$1" message="$2" progress="${3:-null}" error="${4:-}"
|
||||||
@@ -33,6 +38,14 @@ write_status() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Prevent concurrent runs (udev fires multiple times during rpiboot re-enumeration)
|
||||||
|
exec 9>"$LOCK_FILE"
|
||||||
|
if ! flock -n 9; then
|
||||||
|
echo "[$(date -Iseconds)] Another flash-emmc instance is already running; exiting."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
trap 'rm -f "$LOCK_FILE" "$CURRENT_DEVICE_FILE" "$DEVICE_SOURCE_FILE" 2>/dev/null; exec 9>&-' EXIT
|
||||||
|
|
||||||
# Optional: only run if this file exists (safety)
|
# Optional: only run if this file exists (safety)
|
||||||
ENABLE_FILE="${ENABLE_FILE:-/etc/cm4-provisioning/enabled}"
|
ENABLE_FILE="${ENABLE_FILE:-/etc/cm4-provisioning/enabled}"
|
||||||
if [[ -n "$ENABLE_FILE" && ! -f "$ENABLE_FILE" ]]; then
|
if [[ -n "$ENABLE_FILE" && ! -f "$ENABLE_FILE" ]]; then
|
||||||
@@ -44,6 +57,10 @@ fi
|
|||||||
# When a device is detected we ask the user (dashboard): Backup or Deploy?
|
# When a device is detected we ask the user (dashboard): Backup or Deploy?
|
||||||
# These files are used to wait for the user's choice (written by dashboard, read by this script).
|
# These files are used to wait for the user's choice (written by dashboard, read by this script).
|
||||||
BACKUPS_DIR="${BACKUPS_DIR:-/var/lib/cm4-provisioning/backups}"
|
BACKUPS_DIR="${BACKUPS_DIR:-/var/lib/cm4-provisioning/backups}"
|
||||||
|
# Optional: shrink backup with PiShrink (requires pishrink + parted, e2fsprogs). SHRINK_BACKUP=1 to enable.
|
||||||
|
SHRINK_BACKUP="${SHRINK_BACKUP:-0}"
|
||||||
|
# If SHRINK_BACKUP=1, optionally compress: PISHRINK_COMPRESS=gz or xz (uses parallel when available). Uncompressed .img can be dd'd directly for deploy.
|
||||||
|
PISHRINK_COMPRESS="${PISHRINK_COMPRESS:-}"
|
||||||
ACTION_REQUEST_FILE="${ACTION_REQUEST_FILE:-/var/lib/cm4-provisioning/action_request}"
|
ACTION_REQUEST_FILE="${ACTION_REQUEST_FILE:-/var/lib/cm4-provisioning/action_request}"
|
||||||
CURRENT_DEVICE_FILE="${CURRENT_DEVICE_FILE:-/var/lib/cm4-provisioning/current_device}"
|
CURRENT_DEVICE_FILE="${CURRENT_DEVICE_FILE:-/var/lib/cm4-provisioning/current_device}"
|
||||||
DEVICE_SOURCE_FILE="${DEVICE_SOURCE_FILE:-/var/lib/cm4-provisioning/device_source}"
|
DEVICE_SOURCE_FILE="${DEVICE_SOURCE_FILE:-/var/lib/cm4-provisioning/device_source}"
|
||||||
@@ -72,6 +89,12 @@ if [[ -z "$RPIBOOT_GADGET" ]]; then
|
|||||||
write_status "error" "rpiboot gadget missing" "null" "Copy mass-storage-gadget(64) to $RPIBOOT_DIR"
|
write_status "error" "rpiboot gadget missing" "null" "Copy mass-storage-gadget(64) to $RPIBOOT_DIR"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
# rpiboot requires bootfiles.bin or one of bootcode*.bin in the gadget dir; empty dir causes "No 'bootcode' files found"
|
||||||
|
if [[ ! -f "$RPIBOOT_GADGET/bootfiles.bin" && ! -f "$RPIBOOT_GADGET/bootcode.bin" && ! -f "$RPIBOOT_GADGET/bootcode4.bin" && ! -f "$RPIBOOT_GADGET/bootcode5.bin" ]]; then
|
||||||
|
log "rpiboot gadget dir has no boot files: $RPIBOOT_GADGET (reinstall usbboot)"
|
||||||
|
write_status "error" "rpiboot gadget empty" "null" "No boot files in $RPIBOOT_GADGET. Reinstall usbboot: run install-usbboot-on-host.sh on the host or build-and-deploy-usbboot-to-host.sh from your machine."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Ensure status dir exists and start with running state
|
# Ensure status dir exists and start with running state
|
||||||
mkdir -p "$(dirname "$STATUS_FILE")" "$BACKUPS_DIR" 2>/dev/null || true
|
mkdir -p "$(dirname "$STATUS_FILE")" "$BACKUPS_DIR" 2>/dev/null || true
|
||||||
@@ -81,35 +104,47 @@ write_status "rpiboot" "Connecting to CM4 in boot mode…" "0"
|
|||||||
before_devs=$(lsblk -nd -o NAME 2>/dev/null | sort)
|
before_devs=$(lsblk -nd -o NAME 2>/dev/null | sort)
|
||||||
|
|
||||||
log "Starting rpiboot to expose CM4 eMMC as mass storage..."
|
log "Starting rpiboot to expose CM4 eMMC as mass storage..."
|
||||||
if ! "$RPIBOOT_BIN" -d "$RPIBOOT_GADGET"; then
|
# Run rpiboot with 90s timeout so we don't hang if it doesn't exit cleanly when device switches to mass storage
|
||||||
log "rpiboot failed or no device connected"
|
rpiboot_exit=0
|
||||||
write_status "error" "rpiboot failed" "null" "rpiboot failed or no device connected"
|
timeout 90 "$RPIBOOT_BIN" -d "$RPIBOOT_GADGET" || rpiboot_exit=$?
|
||||||
|
# timeout returns 124 if killed by timeout; 0 or other if rpiboot exited on its own
|
||||||
|
if [[ "$rpiboot_exit" -eq 124 ]]; then
|
||||||
|
log "rpiboot timed out after 90s (device may have switched to mass storage)"
|
||||||
|
elif [[ "$rpiboot_exit" -ne 0 ]]; then
|
||||||
|
log "rpiboot exited with code $rpiboot_exit"
|
||||||
|
write_status "error" "rpiboot failed" "null" "rpiboot failed or no device connected. Check flash.log on host. Try unplug/replug USB."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# rpiboot exits when mass storage appears; give udev a moment to create /dev/sdX
|
echo "[$(date -Iseconds)] rpiboot finished (exit=$rpiboot_exit); starting device scan"
|
||||||
sleep 3
|
log "rpiboot completed; waiting for block device..."
|
||||||
|
write_status "rpiboot" "rpiboot done, waiting for block device…" "10"
|
||||||
|
|
||||||
# Find new block device (prefer one matching expected eMMC size)
|
# rpiboot exits when device switches to mass storage; udev may need several seconds to create /dev/sdX
|
||||||
|
# Poll for new block device for up to 30s (device switch can be slow)
|
||||||
target_dev=""
|
target_dev=""
|
||||||
for dev in /dev/sd[a-z] /dev/sd[a-z][a-z]; do
|
for wait_sec in $(seq 2 2 10) $(seq 12 2 30); do
|
||||||
[[ -b "$dev" ]] || continue
|
sleep 2
|
||||||
# Skip partitions
|
for dev in /dev/sd[a-z] /dev/sd[a-z][a-z]; do
|
||||||
[[ "$dev" =~ [0-9]$ ]] && continue
|
[[ -b "$dev" ]] || continue
|
||||||
size=$(blockdev --getsize64 "$dev" 2>/dev/null || true)
|
[[ "$dev" =~ [0-9]$ ]] && continue
|
||||||
if [[ -n "$size" ]]; then
|
size=$(blockdev --getsize64 "$dev" 2>/dev/null || true)
|
||||||
# Allow 5% tolerance on size
|
if [[ -n "$size" ]]; then
|
||||||
if (( size >= EMMC_SIZE_BYTES * 95 / 100 && size <= EMMC_SIZE_BYTES * 105 / 100 )); then
|
if (( size >= EMMC_SIZE_BYTES * 95 / 100 && size <= EMMC_SIZE_BYTES * 105 / 100 )); then
|
||||||
target_dev=$dev
|
target_dev=$dev
|
||||||
break
|
break 2
|
||||||
|
fi
|
||||||
|
if [[ -z "$target_dev" && "$before_devs" != *"${dev#/dev/}"* ]]; then
|
||||||
|
target_dev=$dev
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
# Otherwise take first new disk that appeared (fallback)
|
done
|
||||||
if [[ -z "$target_dev" && "$before_devs" != *"${dev#/dev/}"* ]]; then
|
[[ -n "$target_dev" ]] && break
|
||||||
target_dev=$dev
|
log "Waiting for block device... ${wait_sec}s"
|
||||||
fi
|
write_status "rpiboot" "Waiting for eMMC block device… (${wait_sec}s)" "10"
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
|
|
||||||
|
log "Device scan complete. before_devs=[$before_devs] target_dev=[$target_dev]"
|
||||||
if [[ -z "$target_dev" ]]; then
|
if [[ -z "$target_dev" ]]; then
|
||||||
log "No suitable block device found after rpiboot (expected ~${EMMC_SIZE_BYTES} bytes)"
|
log "No suitable block device found after rpiboot (expected ~${EMMC_SIZE_BYTES} bytes)"
|
||||||
write_status "error" "No eMMC device found" "null" "No suitable block device after rpiboot"
|
write_status "error" "No eMMC device found" "null" "No suitable block device after rpiboot"
|
||||||
@@ -131,9 +166,45 @@ for (( i = 0; i < WAIT_TIMEOUT; i += 2 )); do
|
|||||||
backup_path="$BACKUPS_DIR/$backup_name"
|
backup_path="$BACKUPS_DIR/$backup_name"
|
||||||
write_status "backup" "Creating backup…" "null"
|
write_status "backup" "Creating backup…" "null"
|
||||||
log "Backing up $target_dev to $backup_path..."
|
log "Backing up $target_dev to $backup_path..."
|
||||||
if dd if="$target_dev" of="$backup_path" bs=4M status=progress conv=fsync 2>>"$LOG_FILE"; then
|
if dd if="$target_dev" of="$backup_path" bs=4M status=progress conv=fsync; then
|
||||||
log "Backup complete: $backup_path"
|
final_name="$backup_name"
|
||||||
write_status "done" "Backup complete: $backup_name" "100"
|
shrink_requested=false
|
||||||
|
[[ "$SHRINK_BACKUP" == "1" || "$SHRINK_BACKUP" == "true" ]] && shrink_requested=true
|
||||||
|
[[ -f "$(dirname "$STATUS_FILE")/shrink_next_backup" ]] && shrink_requested=true
|
||||||
|
rm -f "$(dirname "$STATUS_FILE")/shrink_next_backup" 2>/dev/null || true
|
||||||
|
if [[ "$shrink_requested" == "true" ]]; then
|
||||||
|
pishrink_cmd=""
|
||||||
|
for p in /usr/local/bin/pishrink.sh /usr/local/bin/pishrink; do
|
||||||
|
[[ -x "$p" ]] && pishrink_cmd="$p" && break
|
||||||
|
done
|
||||||
|
if [[ -n "$pishrink_cmd" ]]; then
|
||||||
|
write_status "backup" "Shrinking backup image…" "null"
|
||||||
|
log "Shrinking backup with PiShrink (timeout 30 min)..."
|
||||||
|
pishrink_opts="-n"
|
||||||
|
[[ "$PISHRINK_COMPRESS" == "gz" || "$PISHRINK_COMPRESS" == "gzip" ]] && pishrink_opts="$pishrink_opts -z -a"
|
||||||
|
[[ "$PISHRINK_COMPRESS" == "xz" ]] && pishrink_opts="$pishrink_opts -Z -a"
|
||||||
|
PISHRINK_TIMEOUT="${PISHRINK_TIMEOUT:-1800}"
|
||||||
|
if timeout "$PISHRINK_TIMEOUT" $pishrink_cmd $pishrink_opts "$backup_path" 2>&1; then
|
||||||
|
if [[ "$PISHRINK_COMPRESS" == "gz" || "$PISHRINK_COMPRESS" == "gzip" ]]; then
|
||||||
|
final_name="${backup_name}.gz"
|
||||||
|
elif [[ "$PISHRINK_COMPRESS" == "xz" ]]; then
|
||||||
|
final_name="${backup_name}.xz"
|
||||||
|
fi
|
||||||
|
log "Shrunk backup: $BACKUPS_DIR/$final_name"
|
||||||
|
else
|
||||||
|
rc=$?
|
||||||
|
if [[ "$rc" -eq 124 ]]; then
|
||||||
|
log "PiShrink timed out after ${PISHRINK_TIMEOUT}s; keeping full backup $backup_path"
|
||||||
|
else
|
||||||
|
log "PiShrink failed (exit $rc); keeping full backup $backup_path"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "SHRINK_BACKUP=1 but pishrink not found; keeping full backup. Install with scripts/install-pishrink-on-host.sh"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
log "Backup complete: $BACKUPS_DIR/$final_name"
|
||||||
|
write_status "done" "Backup complete: $final_name" "100"
|
||||||
else
|
else
|
||||||
write_status "error" "Backup failed" "null" "dd failed"
|
write_status "error" "Backup failed" "null" "dd failed"
|
||||||
fi
|
fi
|
||||||
@@ -141,12 +212,11 @@ for (( i = 0; i < WAIT_TIMEOUT; i += 2 )); do
|
|||||||
if [[ ! -f "$GOLDEN_IMAGE" ]]; then
|
if [[ ! -f "$GOLDEN_IMAGE" ]]; then
|
||||||
log "Golden image not found; cannot deploy."
|
log "Golden image not found; cannot deploy."
|
||||||
write_status "error" "Deploy unavailable" "null" "Golden image not found. Add golden.img to /var/lib/cm4-provisioning/ for deploy."
|
write_status "error" "Deploy unavailable" "null" "Golden image not found. Add golden.img to /var/lib/cm4-provisioning/ for deploy."
|
||||||
rm -f "$CURRENT_DEVICE_FILE" "$DEVICE_SOURCE_FILE" 2>/dev/null || true
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
write_status "flashing" "Writing golden image…" "null"
|
write_status "flashing" "Writing golden image…" "null"
|
||||||
log "Flashing $GOLDEN_IMAGE to $target_dev..."
|
log "Flashing $GOLDEN_IMAGE to $target_dev..."
|
||||||
if dd if="$GOLDEN_IMAGE" of="$target_dev" bs=4M status=progress conv=fsync 2>>"$LOG_FILE"; then
|
if dd if="$GOLDEN_IMAGE" of="$target_dev" bs=4M status=progress conv=fsync; then
|
||||||
log "Flash complete. Remove eMMC disable jumper and power cycle the reTerminal."
|
log "Flash complete. Remove eMMC disable jumper and power cycle the reTerminal."
|
||||||
write_status "done" "Flash complete. Remove eMMC disable jumper and power cycle the reTerminal." "100"
|
write_status "done" "Flash complete. Remove eMMC disable jumper and power cycle the reTerminal." "100"
|
||||||
else
|
else
|
||||||
@@ -155,11 +225,9 @@ for (( i = 0; i < WAIT_TIMEOUT; i += 2 )); do
|
|||||||
else
|
else
|
||||||
write_status "error" "Unknown action" "null" "action_request must be 'backup' or 'deploy'"
|
write_status "error" "Unknown action" "null" "action_request must be 'backup' or 'deploy'"
|
||||||
fi
|
fi
|
||||||
rm -f "$CURRENT_DEVICE_FILE" "$DEVICE_SOURCE_FILE" 2>/dev/null || true
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
log "Timeout waiting for user choice"
|
log "Timeout waiting for user choice"
|
||||||
write_status "idle" "Timeout waiting for choice. Connect the device again to retry." "null"
|
write_status "idle" "Timeout waiting for choice. Connect the device again to retry." "null"
|
||||||
rm -f "$CURRENT_DEVICE_FILE" "$DEVICE_SOURCE_FILE" 2>/dev/null || true
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -41,11 +41,16 @@ rsync -a "$BUILD_DIR/usbboot/rpiboot" "$PROXMOX:/opt/usbboot/"
|
|||||||
# CM4 needs mass-storage-gadget (64-bit); copy whichever exists
|
# CM4 needs mass-storage-gadget (64-bit); copy whichever exists
|
||||||
for dir in mass-storage-gadget64 mass-storage-gadget; do
|
for dir in mass-storage-gadget64 mass-storage-gadget; do
|
||||||
if [[ -d "$BUILD_DIR/usbboot/$dir" ]]; then
|
if [[ -d "$BUILD_DIR/usbboot/$dir" ]]; then
|
||||||
rsync -a "$BUILD_DIR/usbboot/$dir" "$PROXMOX:/opt/usbboot/"
|
rsync -a "$BUILD_DIR/usbboot/$dir/" "$PROXMOX:/opt/usbboot/$dir/"
|
||||||
echo " Copied $dir/"
|
echo " Copied $dir/"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
ssh "$PROXMOX" "chmod +x /opt/usbboot/rpiboot"
|
ssh "$PROXMOX" "chmod +x /opt/usbboot/rpiboot"
|
||||||
|
|
||||||
|
# Verify gadget has boot files (rpiboot needs bootfiles.bin or bootcode*.bin)
|
||||||
|
if ! ssh "$PROXMOX" "test -f /opt/usbboot/mass-storage-gadget64/bootfiles.bin || test -f /opt/usbboot/mass-storage-gadget64/boot.img" 2>/dev/null; then
|
||||||
|
echo "Warning: mass-storage-gadget64 may be missing boot files. If rpiboot fails with 'No bootcode files found', run: ./populate-gadget-on-host.sh $PROXMOX"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "[$(date -Iseconds)] usbboot deployed to $PROXMOX:/opt/usbboot"
|
echo "[$(date -Iseconds)] usbboot deployed to $PROXMOX:/opt/usbboot"
|
||||||
echo "Ensure the host flash script runs rpiboot with: -d /opt/usbboot/mass-storage-gadget64 (or mass-storage-gadget)."
|
echo "Ensure the host flash script runs rpiboot with: -d /opt/usbboot/mass-storage-gadget64 (or mass-storage-gadget)."
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
# Deploy CM4 eMMC provisioning to a Proxmox host (creates LXC 201, installs scripts on host and in LXC).
|
# Deploy CM4 eMMC provisioning to a Proxmox host (creates LXC 201, installs scripts on host and in LXC).
|
||||||
# Usage: ./deploy-to-proxmox.sh [proxmox_host]
|
# Usage: ./deploy-to-proxmox.sh [proxmox_host]
|
||||||
# Example: ./deploy-to-proxmox.sh root@10.130.60.224
|
# Example: ./deploy-to-proxmox.sh root@10.130.60.224
|
||||||
|
# Optional: DEPLOY_ROOTFS_STORAGE=local-lvm (or local-zfs, etc.) — storage for LXC rootfs
|
||||||
|
# Optional: CM4_BACKUPS_HOST_PATH=/mnt/storage/cm4-backups — host dir for backup images; bind-mounted into LXC so images are stored on the host
|
||||||
# Requires: ssh key access to root@<host>
|
# Requires: ssh key access to root@<host>
|
||||||
# Logging: set DEPLOY_LOG=1 to also write to deploy-YYYYMMDD-HHMMSS.log in the script dir.
|
# Logging: set DEPLOY_LOG=1 to also write to deploy-YYYYMMDD-HHMMSS.log in the script dir.
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
PROXMOX="${1:-root@10.130.60.224}"
|
PROXMOX="${1:-root@10.130.60.224}"
|
||||||
|
ROOTFS_STORAGE="${DEPLOY_ROOTFS_STORAGE:-local-lvm}"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
REPO_DIR="$(dirname "$SCRIPT_DIR")"
|
REPO_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
LOG_FILE=""
|
LOG_FILE=""
|
||||||
@@ -26,17 +29,28 @@ log "[2/4] Rsync repo to $PROXMOX ..."
|
|||||||
rsync -a "$REPO_DIR/" "$PROXMOX:/tmp/emmc-provisioning-deploy/" --exclude='.git' --exclude='scripts/deploy-to-proxmox.sh'
|
rsync -a "$REPO_DIR/" "$PROXMOX:/tmp/emmc-provisioning-deploy/" --exclude='.git' --exclude='scripts/deploy-to-proxmox.sh'
|
||||||
log "[3/4] Running remote install (host + LXC) ..."
|
log "[3/4] Running remote install (host + LXC) ..."
|
||||||
|
|
||||||
ssh "$PROXMOX" bash -s << 'REMOTE'
|
ssh "$PROXMOX" "ROOTFS_STORAGE='$ROOTFS_STORAGE' CM4_BACKUPS_HOST_PATH='${CM4_BACKUPS_HOST_PATH:-}'" bash -s << 'REMOTE'
|
||||||
set -e
|
set -e
|
||||||
DEPLOY=/tmp/emmc-provisioning-deploy
|
DEPLOY=/tmp/emmc-provisioning-deploy
|
||||||
|
ROOTFS_STORAGE="${ROOTFS_STORAGE:-local-lvm}"
|
||||||
|
BACKUPS_HOST_PATH="${CM4_BACKUPS_HOST_PATH:-}"
|
||||||
log() { echo "[$(date -Iseconds)] $*"; }
|
log() { echo "[$(date -Iseconds)] $*"; }
|
||||||
|
|
||||||
# Ensure LXC 201 exists (create if not)
|
# Ensure LXC 201 exists (create if not)
|
||||||
if ! pct status 201 &>/dev/null; then
|
if ! pct status 201 &>/dev/null; then
|
||||||
log "Creating LXC 201 (cm4-provisioning)..."
|
# Use Debian 12 template: prefer one in cache, else download latest
|
||||||
pct create 201 local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \
|
VZTMPL_DIR=/var/lib/vz/template/cache
|
||||||
|
DEBIAN12_TMPL=$(ls "$VZTMPL_DIR"/debian-12-standard_*.tar.zst 2>/dev/null | head -1)
|
||||||
|
if [[ -z "$DEBIAN12_TMPL" ]]; then
|
||||||
|
log "Downloading Debian 12 LXC template..."
|
||||||
|
pveam download local debian-12-standard_12.12-1_amd64.tar.zst || pveam download local debian-12-standard_12.7-1_amd64.tar.zst
|
||||||
|
DEBIAN12_TMPL=$(ls "$VZTMPL_DIR"/debian-12-standard_*.tar.zst 2>/dev/null | head -1)
|
||||||
|
fi
|
||||||
|
TMPL_NAME=$(basename "$DEBIAN12_TMPL")
|
||||||
|
log "Creating LXC 201 (cm4-provisioning) (rootfs on ${ROOTFS_STORAGE}, template ${TMPL_NAME})..."
|
||||||
|
pct create 201 "local:vztmpl/${TMPL_NAME}" \
|
||||||
--hostname cm4-provisioning --memory 1024 --swap 0 --cores 1 \
|
--hostname cm4-provisioning --memory 1024 --swap 0 --cores 1 \
|
||||||
--rootfs local-zfs:8 --net0 name=eth0,bridge=vmbr0,ip=dhcp \
|
--rootfs "${ROOTFS_STORAGE}:8" --net0 name=eth0,bridge=vmbr0,ip=dhcp \
|
||||||
--unprivileged 0 --features nesting=1 -tag cm4-provisioning
|
--unprivileged 0 --features nesting=1 -tag cm4-provisioning
|
||||||
mkdir -p /var/lib/cm4-provisioning
|
mkdir -p /var/lib/cm4-provisioning
|
||||||
pct set 201 -mp0 /var/lib/cm4-provisioning,mp=/var/lib/cm4-provisioning
|
pct set 201 -mp0 /var/lib/cm4-provisioning,mp=/var/lib/cm4-provisioning
|
||||||
@@ -45,6 +59,15 @@ else
|
|||||||
log "LXC 201 already exists."
|
log "LXC 201 already exists."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Optional: bind-mount a host directory for backup images (so they are stored on the host, not LXC rootfs)
|
||||||
|
if [[ -n "$BACKUPS_HOST_PATH" ]]; then
|
||||||
|
mkdir -p "$BACKUPS_HOST_PATH"
|
||||||
|
pct stop 201 2>/dev/null || true
|
||||||
|
pct set 201 -mp1 "$BACKUPS_HOST_PATH",mp=/var/lib/cm4-provisioning/backups
|
||||||
|
pct start 201 2>/dev/null || true
|
||||||
|
log "Backups mount: host $BACKUPS_HOST_PATH -> LXC /var/lib/cm4-provisioning/backups"
|
||||||
|
fi
|
||||||
|
|
||||||
# Host: install scripts and udev (from host/)
|
# Host: install scripts and udev (from host/)
|
||||||
log "Host: installing scripts and udev ..."
|
log "Host: installing scripts and udev ..."
|
||||||
mkdir -p /opt/cm4-provisioning /etc/cm4-provisioning
|
mkdir -p /opt/cm4-provisioning /etc/cm4-provisioning
|
||||||
@@ -52,6 +75,9 @@ cp "$DEPLOY/host/flash-emmc-on-connect.sh" /opt/cm4-provisioning/
|
|||||||
chmod +x /opt/cm4-provisioning/flash-emmc-on-connect.sh
|
chmod +x /opt/cm4-provisioning/flash-emmc-on-connect.sh
|
||||||
cp "$DEPLOY/host/cm4-flash-trigger.sh" /usr/local/bin/
|
cp "$DEPLOY/host/cm4-flash-trigger.sh" /usr/local/bin/
|
||||||
chmod +x /usr/local/bin/cm4-flash-trigger.sh
|
chmod +x /usr/local/bin/cm4-flash-trigger.sh
|
||||||
|
cp "$DEPLOY/host/cm4-flash.service" /etc/systemd/system/
|
||||||
|
systemctl daemon-reload
|
||||||
|
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/
|
cp "$DEPLOY/host/90-cm4-boot-mode.rules" /etc/udev/rules.d/
|
||||||
udevadm control --reload-rules
|
udevadm control --reload-rules
|
||||||
log "Host: env and dirs ..."
|
log "Host: env and dirs ..."
|
||||||
@@ -60,8 +86,10 @@ GOLDEN_IMAGE=/var/lib/cm4-provisioning/golden.img
|
|||||||
RPIBOOT_DIR=/opt/usbboot
|
RPIBOOT_DIR=/opt/usbboot
|
||||||
EMMC_SIZE_BYTES=8589934592
|
EMMC_SIZE_BYTES=8589934592
|
||||||
ENV
|
ENV
|
||||||
|
[[ -n "$BACKUPS_HOST_PATH" ]] && echo "BACKUPS_DIR=$BACKUPS_HOST_PATH" >> /opt/cm4-provisioning/env
|
||||||
touch /etc/cm4-provisioning/enabled
|
touch /etc/cm4-provisioning/enabled
|
||||||
mkdir -p /var/lib/cm4-provisioning/backups
|
mkdir -p /var/lib/cm4-provisioning/backups
|
||||||
|
[[ -n "$BACKUPS_HOST_PATH" ]] && mkdir -p "$BACKUPS_HOST_PATH"
|
||||||
|
|
||||||
# Start LXC if stopped
|
# Start LXC if stopped
|
||||||
log "Starting LXC 201 if stopped ..."
|
log "Starting LXC 201 if stopped ..."
|
||||||
@@ -98,6 +126,7 @@ REMOTE
|
|||||||
log "[4/4] Deploy finished."
|
log "[4/4] Deploy finished."
|
||||||
echo ""
|
echo ""
|
||||||
echo "Done. Put golden.img in /var/lib/cm4-provisioning/ on the host (or scp to LXC 201 at /var/lib/cm4-provisioning/)."
|
echo "Done. Put golden.img in /var/lib/cm4-provisioning/ on the host (or scp to LXC 201 at /var/lib/cm4-provisioning/)."
|
||||||
|
[[ -n "${CM4_BACKUPS_HOST_PATH:-}" ]] && echo "Backup images are stored on the host at: $CM4_BACKUPS_HOST_PATH (bind-mounted into LXC at /var/lib/cm4-provisioning/backups)."
|
||||||
echo "When the host has internet, run on the host: bash /tmp/emmc-provisioning-deploy/scripts/install-usbboot-on-host.sh"
|
echo "When the host has internet, run on the host: bash /tmp/emmc-provisioning-deploy/scripts/install-usbboot-on-host.sh"
|
||||||
echo "Dashboard: install flask in LXC 201 and enable cm4-dashboard.service (see docs/PROXMOX-LXC-DEPLOYMENT.md)."
|
echo "Dashboard: install flask in LXC 201 and enable cm4-dashboard.service (see docs/PROXMOX-LXC-DEPLOYMENT.md)."
|
||||||
if [[ -n "$LOG_FILE" ]]; then
|
if [[ -n "$LOG_FILE" ]]; then
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Run on the Proxmox host (as root) when rpiboot fails with "No 'bootcode' files found".
|
||||||
|
# Cause: mass-storage-gadget64/bootfiles.bin is a broken symlink (-> ../firmware/bootfiles.bin).
|
||||||
|
# This script removes the symlink and extracts bootcode4.bin from the installed rpiboot binary.
|
||||||
|
#
|
||||||
|
# On host: bash fix-gadget-bootcode-on-host.sh
|
||||||
|
# From your machine: ssh root@10.130.60.224 'bash -s' < scripts/fix-gadget-bootcode-on-host.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
GADGET="${1:-/opt/usbboot/mass-storage-gadget64}"
|
||||||
|
RPIBOOT="${2:-/opt/usbboot/rpiboot}"
|
||||||
|
|
||||||
|
[[ -d "$GADGET" ]] || { echo "Error: $GADGET not found"; exit 1; }
|
||||||
|
[[ -x "$RPIBOOT" ]] || { echo "Error: $RPIBOOT not found"; exit 1; }
|
||||||
|
|
||||||
|
# Remove broken bootfiles.bin symlink if present
|
||||||
|
if [[ -L "$GADGET/bootfiles.bin" ]] && ! [[ -f "$GADGET/bootfiles.bin" ]]; then
|
||||||
|
rm -f "$GADGET/bootfiles.bin"
|
||||||
|
echo "Removed broken symlink $GADGET/bootfiles.bin"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If we already have a valid boot file, done
|
||||||
|
if [[ -f "$GADGET/bootcode4.bin" ]] || [[ -f "$GADGET/bootfiles.bin" ]]; then
|
||||||
|
echo "Already has bootcode4.bin or valid bootfiles.bin. OK."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get .data section file offset and address from ELF
|
||||||
|
readelf -S "$RPIBOOT" | grep -q "\.data" || { echo "No .data section"; exit 1; }
|
||||||
|
DATA_OFF=$(readelf -S "$RPIBOOT" | awk '/\.data\s/ { print "0x"$5; exit }')
|
||||||
|
DATA_ADDR=$(readelf -S "$RPIBOOT" | awk '/\.data\s/ { print "0x"$4; exit }')
|
||||||
|
LEN_SYM=$(objdump -t "$RPIBOOT" | awk '/msd_bootcode4_bin_len/ { print $1; exit }')
|
||||||
|
BIN_SYM=$(objdump -t "$RPIBOOT" | awk '/msd_bootcode4_bin\s/ { print $1; exit }')
|
||||||
|
[[ -n "$LEN_SYM" && -n "$BIN_SYM" ]] || { echo "msd_bootcode4_bin symbols not found in rpiboot"; exit 1; }
|
||||||
|
|
||||||
|
LEN_ADDR=$((0x$LEN_SYM))
|
||||||
|
BIN_ADDR=$((0x$BIN_SYM))
|
||||||
|
LEN_OFF=$((DATA_OFF + LEN_ADDR - DATA_ADDR))
|
||||||
|
BIN_OFF=$((DATA_OFF + BIN_ADDR - DATA_ADDR))
|
||||||
|
LEN=$(dd if="$RPIBOOT" bs=1 skip=$LEN_OFF count=4 2>/dev/null | od -An -t u4 | tr -d ' ')
|
||||||
|
dd if="$RPIBOOT" of="$GADGET/bootcode4.bin" bs=1 skip=$BIN_OFF count=$LEN 2>/dev/null
|
||||||
|
echo "Wrote $GADGET/bootcode4.bin ($LEN bytes). Verify: $RPIBOOT -d $GADGET -V"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 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
|
||||||
|
|
||||||
|
set -e
|
||||||
|
PISHRINK_URL="${PISHRINK_URL:-https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh}"
|
||||||
|
|
||||||
|
echo "Installing PiShrink dependencies..."
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y wget parted gzip pigz xz-utils udev e2fsprogs
|
||||||
|
|
||||||
|
echo "Downloading PiShrink..."
|
||||||
|
wget -q -O /usr/local/bin/pishrink.sh "$PISHRINK_URL"
|
||||||
|
chmod +x /usr/local/bin/pishrink.sh
|
||||||
|
|
||||||
|
echo "PiShrink installed at /usr/local/bin/pishrink.sh"
|
||||||
|
echo "To shrink backups automatically, add to /opt/cm4-provisioning/env:"
|
||||||
|
echo " SHRINK_BACKUP=1"
|
||||||
|
echo " # optional: PISHRINK_COMPRESS=gz or PISHRINK_COMPRESS=xz"
|
||||||
35
chromium-setup/emmc-provisioning/scripts/populate-gadget-on-host.sh
Executable file
35
chromium-setup/emmc-provisioning/scripts/populate-gadget-on-host.sh
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Populate /opt/usbboot/mass-storage-gadget64 on the Proxmox host (no rpiboot build).
|
||||||
|
# Use when rpiboot works but you see "No 'bootcode' files found" — the gadget dir is empty.
|
||||||
|
# Usage: ./populate-gadget-on-host.sh [proxmox_host]
|
||||||
|
# Example: ./populate-gadget-on-host.sh root@10.130.60.224
|
||||||
|
|
||||||
|
set -e
|
||||||
|
PROXMOX="${1:-root@10.130.60.224}"
|
||||||
|
BUILD_DIR="/tmp/usbboot-gadget-$$"
|
||||||
|
cleanup() { rm -rf "$BUILD_DIR"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
echo "[$(date -Iseconds)] Cloning usbboot to fetch mass-storage-gadget64 ..."
|
||||||
|
mkdir -p "$BUILD_DIR"
|
||||||
|
git clone --depth=1 https://github.com/raspberrypi/usbboot "$BUILD_DIR/usbboot"
|
||||||
|
|
||||||
|
GADGET="$BUILD_DIR/usbboot/mass-storage-gadget64"
|
||||||
|
if [[ ! -d "$GADGET" ]]; then
|
||||||
|
echo "Error: mass-storage-gadget64 not found in clone."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# rpiboot needs at least bootfiles.bin or bootcode*.bin
|
||||||
|
if [[ ! -f "$GADGET/bootfiles.bin" && ! -f "$GADGET/boot.img" ]]; then
|
||||||
|
echo "Warning: clone has no bootfiles.bin or boot.img. Trying git lfs pull..."
|
||||||
|
(cd "$BUILD_DIR/usbboot" && git lfs pull 2>/dev/null) || true
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$GADGET/bootfiles.bin" && ! -f "$GADGET/boot.img" ]]; then
|
||||||
|
echo "Error: mass-storage-gadget64 still has no boot files. Install git-lfs and retry, or run build-and-deploy-usbboot-to-host.sh for a full deploy."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[$(date -Iseconds)] Syncing mass-storage-gadget64 to $PROXMOX:/opt/usbboot/ ..."
|
||||||
|
ssh "$PROXMOX" "mkdir -p /opt/usbboot"
|
||||||
|
rsync -a "$GADGET/" "$PROXMOX:/opt/usbboot/mass-storage-gadget64/"
|
||||||
|
echo "Done. Verify with: ssh $PROXMOX 'ls -la /opt/usbboot/mass-storage-gadget64/'"
|
||||||
Reference in New Issue
Block a user