diff --git a/.gitignore b/.gitignore index 645aec8..a8273d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ gnss-bootstrap-20260223-215010.img.xz +gnss-bootstrap-20260223-215010.img.xz.bak diff --git a/emmc-provisioning/README.md b/emmc-provisioning/README.md index eb3727f..b540da2 100644 --- a/emmc-provisioning/README.md +++ b/emmc-provisioning/README.md @@ -13,9 +13,10 @@ Revisions are tracked project-wide; see repo root **README.md** and `scripts/bum emmc-provisioning/ ├── README.md ← You are here ├── docs/ Documentation +│ ├── DEPLOY-NEW-PROXMOX.md Step-by-step: deploy to a new Proxmox instance │ ├── EMMC-PROVISIONING-GUIDE.md Full setup and usage │ ├── NETWORK-BOOT-LXC.md Network boot (PXE/dnsmasq) and LXC -│ ├── PROXMOX-LXC-DEPLOYMENT.md Proxmox LXC + host setup +│ ├── PROXMOX-LXC-DEPLOYMENT.md Proxmox LXC + host setup (reference) │ └── PORTAL_STYLING_GUIDE.md Dashboard UI styling reference ├── host/ Scripts that run on the provisioning host (Proxmox host) │ ├── flash-emmc-on-connect.sh rpiboot + wait for Backup/Deploy choice, then dd @@ -53,6 +54,7 @@ emmc-provisioning/ ## Quick start 1. **Read** [docs/EMMC-PROVISIONING-GUIDE.md](docs/EMMC-PROVISIONING-GUIDE.md) for setup and usage. -2. **Proxmox:** Use [scripts/deploy-to-proxmox.sh](scripts/deploy-to-proxmox.sh) to deploy to a Proxmox host; see [docs/PROXMOX-LXC-DEPLOYMENT.md](docs/PROXMOX-LXC-DEPLOYMENT.md). -3. **Manual host:** Copy scripts from `host/` to the host and install the udev rule (see the guide). -4. Put **golden.img** in `/var/lib/cm4-provisioning/` (or your configured path). When a device is detected (USB or network), the **dashboard** asks **Backup** or **Deploy**. +2. **Deploy to a new Proxmox:** Follow [docs/DEPLOY-NEW-PROXMOX.md](docs/DEPLOY-NEW-PROXMOX.md) for clear step-by-step instructions. +3. **Proxmox reference:** [scripts/deploy-to-proxmox.sh](scripts/deploy-to-proxmox.sh) and [docs/PROXMOX-LXC-DEPLOYMENT.md](docs/PROXMOX-LXC-DEPLOYMENT.md) for options, layout, and troubleshooting. +4. **Manual host:** Copy scripts from `host/` to the host and install the udev rule (see the guide). +5. Put **golden.img** in `/var/lib/cm4-provisioning/` (or your configured path). When a device is detected (USB or network), the **dashboard** asks **Backup** or **Deploy**. diff --git a/emmc-provisioning/cloud-init/user-data.bootstrap b/emmc-provisioning/cloud-init/user-data.bootstrap index 6e7332b..3501b84 100644 --- a/emmc-provisioning/cloud-init/user-data.bootstrap +++ b/emmc-provisioning/cloud-init/user-data.bootstrap @@ -9,6 +9,12 @@ package_update: true package_upgrade: false +# Keep /etc/hosts in sync with hostname (from meta-data or set below) +manage_etc_hosts: true + +# DNS is managed by systemd-resolved; we do not overwrite /etc/resolv.conf +manage_resolv_conf: false + packages: - curl @@ -19,11 +25,76 @@ write_files: PasswordAuthentication yes PermitRootLogin no + # Push current DHCP DNS into systemd-resolved (for dhcpcd/dhclient when NM doesn't feed resolved). + # With no args: discover DNS from lease or resolvectl and push to resolved for default IF. + # NetworkManager feeds resolved automatically; this covers first boot and non-NM setups. + - path: /usr/local/bin/update-resolv-from-dhcp.sh + content: | + #!/bin/sh + # Push DHCP DNS to systemd-resolved so resolv.conf (stub) uses it. + IF="${IFACE:-$(ip -o -4 route show to default 2>/dev/null | awk '{print $5}' | head -1)}" + [ -z "$IF" ] && exit 0 + DNS="" + if [ -s /run/systemd/resolve/resolv.conf ]; then + DNS=$(grep -E '^nameserver\s+' /run/systemd/resolve/resolv.conf | awk '{print $2}' | tr '\n' ' ') + fi + if [ -z "$DNS" ]; then + DNS=$(resolvectl dns "$IF" 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | tr '\n' ' ') + fi + if [ -z "$DNS" ]; then + LEASE=$(ls /var/lib/dhcp/dhclient.*.leases 2>/dev/null | head -1) + [ -n "$LEASE" ] && DNS=$(grep -oP 'option domain-name-servers \K[^;]+' "$LEASE" 2>/dev/null | tr ',' '\n' | tr -d ' ' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | tr '\n' ' ') + fi + [ -n "$DNS" ] && resolvectl dns "$IF" $DNS + permissions: '0755' + + # dhclient: feed systemd-resolved on every lease acquire/renew (DHCP provides new_domain_name_servers) + - path: /etc/dhcp/dhclient-exit-hooks.d/zzz-update-resolv-conf + content: | + #!/bin/sh + # Run by dhclient on exit; push DHCP DNS into systemd-resolved. + [ -z "$new_domain_name_servers" ] && exit 0 + [ -z "$interface" ] && exit 0 + resolvectl dns "$interface" $new_domain_name_servers + permissions: '0755' + + # NetworkManager: resolved is fed by NM by default; this only runs our script as fallback (e.g. if resolved started late). + - path: /etc/NetworkManager/dispatcher.d/99-update-resolv-from-dhcp + content: | + #!/bin/sh + [ "$2" = "up" ] || [ "$2" = "dhcp4-change" ] || exit 0 + export IFACE="$1" + /usr/local/bin/update-resolv-from-dhcp.sh + permissions: '0755' + + # Tell NetworkManager to send DHCP DNS to systemd-resolved (so every DHCP update is applied). + - path: /etc/NetworkManager/conf.d/99-use-resolved.conf + content: | + [main] + dns=systemd-resolved + rc-manager=unmanaged + + # Fallback: push DHCP DNS to resolved once when network is up (e.g. dhcpcd-only or first boot). + - path: /etc/systemd/system/update-resolv-from-dhcp.service + content: | + [Unit] + Description=Push DHCP DNS to systemd-resolved + After=network-online.target systemd-resolved.service + WantedBy=network-online.target + + [Service] + Type=oneshot + ExecStart=/usr/local/bin/update-resolv-from-dhcp.sh + RemainAfterExit=yes + runcmd: - # Ensure hostname resolves (avoids "sudo: unable to resolve host" when meta-data sets hostname) - - | - H="$(hostname)" - grep -q "127.0.1.1.*$H" /etc/hosts || echo "127.0.1.1 $H" >> /etc/hosts + # Use systemd-resolved for DNS; /etc/resolv.conf -> stub so all lookups go through resolved (DHCP DNS applied by NM/hooks). + - systemctl enable systemd-resolved.service + - systemctl start systemd-resolved.service + - rm -f /etc/resolv.conf && ln -s /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf + # Push current DHCP DNS into resolved once at first boot (in case NM hasn't applied yet). + - /usr/local/bin/update-resolv-from-dhcp.sh + - systemctl enable update-resolv-from-dhcp.service - systemctl enable ssh - systemctl start ssh # Download and run bootstrap script (edit URL to match your file server) diff --git a/emmc-provisioning/docs/DEPLOY-NEW-PROXMOX.md b/emmc-provisioning/docs/DEPLOY-NEW-PROXMOX.md new file mode 100644 index 0000000..910340b --- /dev/null +++ b/emmc-provisioning/docs/DEPLOY-NEW-PROXMOX.md @@ -0,0 +1,152 @@ +# Deploy CM4 eMMC Provisioning to a New Proxmox Instance + +Step-by-step guide to deploy the provisioning service (host + LXC) on a **new** Proxmox server. For redeploy/update and troubleshooting, see [PROXMOX-LXC-DEPLOYMENT.md](PROXMOX-LXC-DEPLOYMENT.md). + +--- + +## Prerequisites (before running the deploy script) + +| Requirement | Details | +|-------------|---------| +| **Proxmox host** | A Proxmox VE node (new or existing) where you want the service. | +| **SSH as root** | You must be able to run `ssh root@YOUR_PROXMOX_HOST` with **key-based auth** (no password prompt). | +| **Proxmox storage** | At least one active storage (e.g. `local` or `local-lvm`). Check on the host: `pvesm status`. | +| **Host internet** (recommended) | Needed so the deploy script can download the Debian 12 LXC template (if missing), and install **usbboot** and **PiShrink** on the host. Without internet, deploy still runs but you must install usbboot and PiShrink manually later. | + +**Optional (set before deploy):** + +- `DEPLOY_ROOTFS_STORAGE=local-lvm` — Skip interactive storage choice when creating the LXC. +- `DEPLOY_LXC_ROOT_PASSWORD=yourpassword` — Set LXC root password and enable SSH. +- `DEPLOY_LXC_SSH_KEY=/path/to/pub` — Copy this key into the LXC (default: `~/.ssh/id_ed25519.pub` or `id_rsa.pub`). +- `CM4_BACKUPS_HOST_PATH=/mnt/storage/cm4-backups` — Store backups on this host path (create the directory on the host if needed). +- **Network (WAN/LAN):** + `DEPLOY_LXC_WAN_BRIDGE=vmbr0` (default), `DEPLOY_LXC_WAN_IP=dhcp` (default), + `DEPLOY_LXC_LAN_BRIDGE=vmbr1`, `DEPLOY_LXC_LAN_SUBNET=10.20.50.1/24` — To add eth1 as provisioning LAN with a custom subnet. + +--- + +## Step 1: Run the deploy script + +From your **workstation** (where the repo is cloned), run: + +```bash +cd /path/to/reTerminal\ DM4 + +./emmc-provisioning/scripts/deploy-to-proxmox.sh root@YOUR_PROXMOX_HOST +``` + +Replace `YOUR_PROXMOX_HOST` with the Proxmox hostname or IP (e.g. `10.20.30.40`). + +**Example with options:** + +```bash +DEPLOY_ROOTFS_STORAGE=local-lvm \ +DEPLOY_LXC_ROOT_PASSWORD='YourSecurePassword' \ +DEPLOY_LXC_LAN_BRIDGE=vmbr1 \ +DEPLOY_LXC_LAN_SUBNET=10.20.50.1/24 \ +./emmc-provisioning/scripts/deploy-to-proxmox.sh root@10.20.30.40 +``` + +- On **first run**, the script will ask you to choose LXC rootfs storage (unless `DEPLOY_ROOTFS_STORAGE` is set). It then creates the LXC, installs host scripts, udev, systemd units, and the dashboard in the LXC. +- The script prints the **LXC IP** at the end. Note it for the next steps (or get it with: + `ssh root@YOUR_PROXMOX_HOST "pct exec \$(pct list -no-header -output vmid,name | awk '\''\$2==\"cm4-provisioning\"{print \$1}'\'') -- hostname -I"`). + +--- + +## Step 2: Install usbboot on the host (if host had no internet during deploy) + +USB flash/backup needs **rpiboot** on the Proxmox **host**. If the deploy log said usbboot install failed or was skipped: + +**From your workstation:** + +```bash +scp emmc-provisioning/scripts/install-usbboot-on-host.sh root@YOUR_PROXMOX_HOST:/tmp/ +ssh root@YOUR_PROXMOX_HOST "bash /tmp/install-usbboot-on-host.sh" +``` + +**Or on the Proxmox host** (if `/tmp/emmc-provisioning-deploy` is still there): + +```bash +ssh root@YOUR_PROXMOX_HOST +bash /tmp/emmc-provisioning-deploy/scripts/install-usbboot-on-host.sh +``` + +--- + +## Step 3: Add a golden image (required for Deploy) + +To **write** an image to a device (Deploy), the host must have a **golden image** at `/var/lib/cm4-provisioning/golden.img`. Backup (read from device) works without it. + +**Option A — From the dashboard** + +1. Open **http://<LXC-IP>:5000** (use the LXC IP from the deploy output). +2. Build a cloud-init image or upload/set an existing backup as golden (see dashboard Admin). + +**Option B — Copy an image from your machine** + +```bash +scp /path/to/your-golden.img root@YOUR_PROXMOX_HOST:/var/lib/cm4-provisioning/golden.img +``` + +--- + +## Step 4: (Optional) SSH into the LXC + +If you set `DEPLOY_LXC_ROOT_PASSWORD` or had a default SSH key, you can already run: + +```bash +ssh root@ +``` + +Otherwise, enable root SSH and add your key: + +```bash +./emmc-provisioning/scripts/setup-lxc-ssh.sh root@YOUR_PROXMOX_HOST +# Or with password: ROOT_PASSWORD='YourPassword' ./emmc-provisioning/scripts/setup-lxc-ssh.sh root@YOUR_PROXMOX_HOST ~/.ssh/id_ed25519.pub +``` + +--- + +## Step 5: (Optional) Network boot (DHCP + TFTP on eth1) + +Only if you deployed with **`DEPLOY_LXC_LAN_BRIDGE`** (and optionally `DEPLOY_LXC_LAN_SUBNET`) and want to offer network boot to devices on that LAN: + +```bash +./emmc-provisioning/scripts/setup-network-boot-on-lxc.sh root@ +``` + +See [NETWORK-BOOT-LXC.md](NETWORK-BOOT-LXC.md) for details. + +--- + +## Step 6: (Optional) Install PiShrink on the host + +If the deploy log said PiShrink install failed (e.g. no internet), and you want **Shrink/Compress** in the dashboard to work: + +```bash +ssh root@YOUR_PROXMOX_HOST "bash /tmp/emmc-provisioning-deploy/scripts/install-pishrink-on-host.sh" +``` + +Or from your machine (stream the script): use the same pattern as in [PROXMOX-LXC-DEPLOYMENT.md](PROXMOX-LXC-DEPLOYMENT.md) for `install-pishrink-on-host.sh`. + +--- + +## Summary checklist + +| Step | Action | Required? | +|------|--------|------------| +| 1 | Run `deploy-to-proxmox.sh root@YOUR_PROXMOX_HOST` | **Yes** | +| 2 | Install usbboot on host (if deploy couldn’t) | For USB flash/backup | +| 3 | Add `golden.img` for Deploy | For Deploy only | +| 4 | SSH to LXC (or use setup-lxc-ssh.sh) | Optional | +| 5 | Run setup-network-boot-on-lxc.sh (if using eth1 LAN) | Optional | +| 6 | Install PiShrink on host (if deploy couldn’t) | For Shrink/Compress | + +**After deployment:** + +- **Dashboard:** http://<LXC-IP>:5000 +- **Golden image path (host and LXC):** `/var/lib/cm4-provisioning/golden.img` +- **Disable auto-flash:** `ssh root@YOUR_PROXMOX_HOST "rm /etc/cm4-provisioning/enabled"` +- **Enable again:** `ssh root@YOUR_PROXMOX_HOST "touch /etc/cm4-provisioning/enabled"` + +Full reference: [PROXMOX-LXC-DEPLOYMENT.md](PROXMOX-LXC-DEPLOYMENT.md). diff --git a/emmc-provisioning/docs/EDIT-CLOUDINIT-ON-DEVICE.md b/emmc-provisioning/docs/EDIT-CLOUDINIT-ON-DEVICE.md index bed94be..3be656d 100644 --- a/emmc-provisioning/docs/EDIT-CLOUDINIT-ON-DEVICE.md +++ b/emmc-provisioning/docs/EDIT-CLOUDINIT-ON-DEVICE.md @@ -1,4 +1,4 @@ -# How to edit cloud-init files on the device before capturing the image +# How to edit cloud-init files on the device or on an image file The cloud-init **NoCloud** files live on the **boot partition**. On the running device they are at: @@ -77,6 +77,34 @@ They are owned by **root** and need **sudo** to edit. --- +## Method 3: Edit on an image file (.img or .img.xz) + +To edit cloud-init **inside a disk image** (e.g. `gnss-bootstrap-20260223-215010.img.xz`) without booting the device: + +1. **Run the helper script** (requires `sudo` for loop device and mount): + + ```bash + cd /path/to/reTerminal-DM4 + ./emmc-provisioning/scripts/edit-cloudinit-on-image.sh gnss-bootstrap-20260223-215010.img.xz + ``` + +2. The script will: + - Decompress the `.img.xz` (if needed; allow ~3GB free space and a minute or two) + - Attach the image with `losetup` and mount the **boot** (FAT32) partition + - Open your `$EDITOR` (or `nano`) on `user-data`, `meta-data`, and `network-config` + - After you save and exit, unmount and recompress, **overwriting** the original `.img.xz` + +3. **Back up the image** before running if you want to keep the original: + ```bash + cp gnss-bootstrap-20260223-215010.img.xz gnss-bootstrap-20260223-215010.img.xz.bak + ``` + +4. **Options:** + - `--no-recompress` — Leave the image decompressed after editing (do not overwrite the `.img.xz`). + - `--replace-with-repo` — Copy `user-data`, `meta-data`, and `network-config` from `emmc-provisioning/cloud-init/` into the image before opening the editor (useful to start from repo templates). + +--- + ## What to edit (typical) - **meta-data** diff --git a/emmc-provisioning/docs/NETWORK-BOOT-LXC.md b/emmc-provisioning/docs/NETWORK-BOOT-LXC.md index 4e33f20..3749164 100644 --- a/emmc-provisioning/docs/NETWORK-BOOT-LXC.md +++ b/emmc-provisioning/docs/NETWORK-BOOT-LXC.md @@ -48,7 +48,13 @@ The script will: ## Proxmox: adding eth1 to the LXC -If you create the container by hand or want a second interface: +Use the deploy script with **`DEPLOY_LXC_LAN_BRIDGE`** and **`DEPLOY_LXC_LAN_SUBNET`** so the LXC is created with eth1 (LAN) from the start: + +```bash +DEPLOY_LXC_LAN_BRIDGE=vmbr1 DEPLOY_LXC_LAN_SUBNET=10.20.50.1/24 ./emmc-provisioning/scripts/deploy-to-proxmox.sh root@YOUR_PROXMOX_HOST +``` + +Or add a second interface to an existing container by hand: 1. On the **Proxmox host**, add a second network device to the container, e.g.: ```bash @@ -65,6 +71,22 @@ If you create the container by hand or want a second interface: Your current LXC already has eth0 (10.130.60.141) and eth1 (10.20.50.1); the setup script only adds DHCP, TFTP, and NAT. +### Changing the LAN subnet + +When you deploy with **`DEPLOY_LXC_LAN_SUBNET`** (e.g. `10.100.1.1/24`), the deploy script writes **`/opt/cm4-provisioning/lan-subnet.conf`** inside the LXC with `LAN_GW`, `LAN_CIDR`, and the DHCP range. All LXC services use this file: + +- **dnsmasq** (DHCP range and, via the toggle script, TFTP next-server) +- **nftables/iptables** (NAT source subnet) +- **toggle-network-boot-dhcp.sh** (option 66/67 next-server) + +So changing `DEPLOY_LXC_LAN_SUBNET` and **re-running the deploy script** updates `lan-subnet.conf`. To apply the new subnet to dnsmasq and NAT, **re-run the setup script** after redeploying: + +```bash +./emmc-provisioning/scripts/setup-network-boot-on-lxc.sh root@ +``` + +Then run **toggle enable** again if you use network boot: `ssh root@ /opt/cm4-provisioning/toggle-network-boot-dhcp.sh enable` + ## After setup: reTerminal network boot 1. Set the reTerminal **boot order** to try eMMC first, then network (e.g. `BOOT_ORDER=0xf21`): use the dashboard **Update EEPROM** when the device is connected via USB boot, or set manually (usbboot recovery / `rpi-eeprom-config` on device). Not set by first-boot. diff --git a/emmc-provisioning/docs/PROXMOX-LXC-DEPLOYMENT.md b/emmc-provisioning/docs/PROXMOX-LXC-DEPLOYMENT.md index fd6e2e2..a1b04d4 100644 --- a/emmc-provisioning/docs/PROXMOX-LXC-DEPLOYMENT.md +++ b/emmc-provisioning/docs/PROXMOX-LXC-DEPLOYMENT.md @@ -2,6 +2,8 @@ The auto-flash **runs on the Proxmox host** (where the USB device appears). The **LXC** holds the dashboard and shares the **golden image** directory with the host. +**New deployment:** For a clear step-by-step guide to deploy on a **new** Proxmox instance, see **[DEPLOY-NEW-PROXMOX.md](DEPLOY-NEW-PROXMOX.md)**. + ## One-command deploy From your repo, a single run deploys **all** host and LXC files (scripts, systemd units, udev, dashboard): @@ -12,6 +14,8 @@ From your repo, a single run deploys **all** host and LXC files (scripts, system Optional env: `CM4_BACKUPS_HOST_PATH=/path`, `DEPLOY_ROOTFS_STORAGE=local-lvm`, `DEPLOY_LXC_ROOT_PASSWORD=secret` (set root password in LXC and enable SSH), `DEPLOY_LXC_SSH_KEY=/path/to/pub` (default: `~/.ssh/id_ed25519.pub` or `id_rsa.pub` — copied to LXC root so you can `ssh root@`). +**Network (WAN / LAN):** `DEPLOY_LXC_WAN_BRIDGE=vmbr0` (default), `DEPLOY_LXC_WAN_IP=dhcp` (default or e.g. `192.168.1.10/24`), `DEPLOY_LXC_LAN_BRIDGE=vmbr1` (if set, add eth1 as LAN), `DEPLOY_LXC_LAN_SUBNET=10.20.50.1/24` (LXC IP on LAN; default `10.20.50.1/24` when LAN bridge is set). Example: `DEPLOY_LXC_WAN_BRIDGE=vmbr0 DEPLOY_LXC_LAN_BRIDGE=vmbr1 DEPLOY_LXC_LAN_SUBNET=10.20.50.1/24 ./scripts/deploy-to-proxmox.sh root@host`. + The script **finds the container by hostname `cm4-provisioning`** (any existing ID). If none exists, it **creates a new LXC with the next available ID**. So you can redeploy repeatedly without assuming a fixed ID like 201. ## What is deployed diff --git a/emmc-provisioning/lxc/README.md b/emmc-provisioning/lxc/README.md index 536f8b9..f6c9870 100644 --- a/emmc-provisioning/lxc/README.md +++ b/emmc-provisioning/lxc/README.md @@ -2,10 +2,12 @@ Config files for the **provisioning LXC** when using **eth1** as a provisioning LAN (DHCP + TFTP for network boot, NAT for internet). +**LAN subnet:** When you deploy with `DEPLOY_LXC_LAN_SUBNET` (e.g. `10.100.1.1/24`), the deploy script writes `/opt/cm4-provisioning/lan-subnet.conf` inside the LXC with `LAN_GW`, `LAN_CIDR`, and `DHCP_RANGE_START`/`DHCP_RANGE_END`. The setup script and toggle script read this file so dnsmasq, NAT, and PXE options all use the same subnet. If the file is missing, defaults are `10.20.50.1/24` and `10.20.50.100`–`10.20.50.200`. + | File | Purpose | |------|--------| -| **dnsmasq-network-boot.conf** | dnsmasq: DHCP + TFTP on eth1 only. Copied to `/etc/dnsmasq.d/` by `scripts/setup-network-boot-on-lxc.sh`. | -| **nft-nat-lan.conf** | nftables NAT so 10.20.50.0/24 uses eth0 for internet. Applied by the setup script to `/etc/nftables.d/nat-lan.conf`. | +| **dnsmasq-network-boot.conf** | Template: dnsmasq DHCP + TFTP on eth1. Setup script writes `/etc/dnsmasq.d/network-boot.conf` using values from `lan-subnet.conf`. | +| **nft-nat-lan.conf** | Template: nftables NAT for LAN→WAN. Setup script writes `/etc/nftables.d/nat-lan.conf` using `LAN_CIDR` from `lan-subnet.conf`. | Setup is done by running (from your machine): diff --git a/emmc-provisioning/lxc/dnsmasq-network-boot-pxe.conf b/emmc-provisioning/lxc/dnsmasq-network-boot-pxe.conf index b51950a..b645e21 100644 --- a/emmc-provisioning/lxc/dnsmasq-network-boot-pxe.conf +++ b/emmc-provisioning/lxc/dnsmasq-network-boot-pxe.conf @@ -1,5 +1,6 @@ # PXE/network-boot DHCP options (option 66 = next-server, 67 = boot file). # When this file is present, dnsmasq advertises network boot; when removed, devices get DHCP only and boot from local storage. # Toggle with: /opt/cm4-provisioning/toggle-network-boot-dhcp.sh enable|disable +# Template; toggle script writes the real next-server from /opt/cm4-provisioning/lan-subnet.conf (LAN_GW). dhcp-option=66,10.20.50.1 dhcp-option=67,start4cd.elf diff --git a/emmc-provisioning/lxc/dnsmasq-network-boot.conf b/emmc-provisioning/lxc/dnsmasq-network-boot.conf index a40c1bd..81f6f04 100644 --- a/emmc-provisioning/lxc/dnsmasq-network-boot.conf +++ b/emmc-provisioning/lxc/dnsmasq-network-boot.conf @@ -1,12 +1,14 @@ # dnsmasq: DHCP + TFTP on eth1 only (provisioning LAN). # Install to /etc/dnsmasq.d/network-boot.conf on the LXC. # Restrict to eth1 so we don't interfere with host/other DHCP. +# When using setup-network-boot-on-lxc.sh, the actual subnet and DHCP range +# come from /opt/cm4-provisioning/lan-subnet.conf (written by deploy-to-proxmox.sh). # Listen only on eth1 (provisioning LAN) interface=eth1 bind-interfaces -# DHCP range for devices on eth1 (adjust if you use a different subnet) +# DHCP range for devices on eth1 (template; setup script uses lan-subnet.conf) dhcp-range=10.20.50.100,10.20.50.200,12h # TFTP for Raspberry Pi / CM4 network boot diff --git a/emmc-provisioning/lxc/nft-nat-lan.conf b/emmc-provisioning/lxc/nft-nat-lan.conf index 9f6cea3..ae1a555 100644 --- a/emmc-provisioning/lxc/nft-nat-lan.conf +++ b/emmc-provisioning/lxc/nft-nat-lan.conf @@ -1,6 +1,6 @@ # nftables: NAT for LAN (eth1) so clients use WAN (eth0) for internet. # Load with: nft -f /etc/nftables.d/nat-lan.conf -# Or use the inline rules in setup-network-boot-on-lxc.sh (no separate file dependency). +# When using setup-network-boot-on-lxc.sh, the subnet is taken from /opt/cm4-provisioning/lan-subnet.conf (LAN_CIDR). table ip nat { chain postrouting { diff --git a/emmc-provisioning/lxc/toggle-network-boot-dhcp.sh b/emmc-provisioning/lxc/toggle-network-boot-dhcp.sh index a2e2c73..427c572 100755 --- a/emmc-provisioning/lxc/toggle-network-boot-dhcp.sh +++ b/emmc-provisioning/lxc/toggle-network-boot-dhcp.sh @@ -3,11 +3,19 @@ # When disabled, TFTP is stopped and no boot server is advertised; DHCP still runs. # Usage: toggle-network-boot-dhcp.sh enable | disable | status # Run as root. Install to /opt/cm4-provisioning/toggle-network-boot-dhcp.sh +# LAN gateway for TFTP/next-server is read from /opt/cm4-provisioning/lan-subnet.conf (written by deploy-to-proxmox.sh). set -e PXE_CONF="/etc/dnsmasq.d/network-boot-pxe.conf" MAIN_CONF="/etc/dnsmasq.d/network-boot.conf" +LAN_CONF="/opt/cm4-provisioning/lan-subnet.conf" +if [[ -f "$LAN_CONF" ]]; then + source "$LAN_CONF" +else + LAN_GW="10.20.50.1" +fi + # Remove enable-tftp / tftp-root from main config if present (legacy; these belong in PXE conf) cleanup_main_conf() { if [ -f "$MAIN_CONF" ] && grep -q 'enable-tftp\|tftp-root' "$MAIN_CONF" 2>/dev/null; then @@ -18,19 +26,19 @@ cleanup_main_conf() { case "${1:-}" in enable) cleanup_main_conf - cat > "$PXE_CONF" << 'EOF' + cat > "$PXE_CONF" << EOF # PXE/network boot ENABLED - managed by toggle-network-boot-dhcp.sh # TFTP server (only active when network boot is enabled) enable-tftp tftp-root=/srv/tftpboot # BOOTP fields (siaddr = TFTP server, filename = boot file) -dhcp-boot=start4cd.elf,,10.20.50.1 +dhcp-boot=start4cd.elf,,${LAN_GW} # DHCP options 66/67 (some PXE clients prefer these) -dhcp-option=66,10.20.50.1 +dhcp-option=66,${LAN_GW} dhcp-option=67,start4cd.elf EOF systemctl restart dnsmasq 2>/dev/null || service dnsmasq restart 2>/dev/null || true - echo "Network boot enabled." + echo "Network boot enabled (TFTP next-server: $LAN_GW)." ;; disable) cleanup_main_conf diff --git a/emmc-provisioning/scripts/deploy-to-proxmox.sh b/emmc-provisioning/scripts/deploy-to-proxmox.sh index 14a72cf..4b42d5e 100755 --- a/emmc-provisioning/scripts/deploy-to-proxmox.sh +++ b/emmc-provisioning/scripts/deploy-to-proxmox.sh @@ -23,6 +23,12 @@ # DEPLOY_LXC_ROOT_PASSWORD=secret — set root password in LXC and enable SSH # DEPLOY_LXC_SSH_KEY=/path/to/pub — copy this key to LXC root (default: ~/.ssh/id_ed25519.pub or id_rsa.pub) # DEPLOY_LOG=1 — also log to deploy-YYYYMMDD-HHMMSS.log +# DEPLOY_LXC_WAN_BRIDGE=vmbr0 — Proxmox bridge for WAN (eth0); default vmbr0 +# DEPLOY_LXC_WAN_IP=dhcp — WAN address: dhcp (default) or static e.g. 192.168.1.10/24 +# DEPLOY_LXC_LAN_BRIDGE=vmbr1 — If set, add eth1 as LAN on this bridge (e.g. provisioning / network-boot) +# DEPLOY_LXC_LAN_SUBNET=10.20.50.1/24 — LXC IP on LAN (gateway); used only if DEPLOY_LXC_LAN_BRIDGE is set; default 10.20.50.1/24 +# +# Legacy: DEPLOY_LXC_NET1="name=eth1,bridge=vmbr1,ip=10.20.50.1/24" still works; overridden by DEPLOY_LXC_LAN_BRIDGE + DEPLOY_LXC_LAN_SUBNET if both are set. # # Requires: ssh key access to root@. For full install (usbboot, PiShrink), host needs internet. @@ -144,8 +150,8 @@ rsync -a "$REPO_DIR/" "$PROXMOX:/tmp/emmc-provisioning-deploy/" --exclude='.git' log "[4/5] Running remote install (host + LXC) ..." -# Pass optional LXC SSH vars (base64) and selected storage -ssh "$PROXMOX" "ROOTFS_STORAGE='$ROOTFS_STORAGE' CM4_BACKUPS_HOST_PATH='${CM4_BACKUPS_HOST_PATH:-}' DEPLOY_SSH_KEY_B64='${DEPLOY_SSH_KEY_B64:-}' DEPLOY_LXC_PWD_B64='${DEPLOY_LXC_PWD_B64:-}'" bash -s << 'REMOTE' +# Pass optional LXC SSH vars (base64), selected storage, and network (WAN/LAN bridge + subnet) +ssh "$PROXMOX" "ROOTFS_STORAGE='$ROOTFS_STORAGE' CM4_BACKUPS_HOST_PATH='${CM4_BACKUPS_HOST_PATH:-}' DEPLOY_SSH_KEY_B64='${DEPLOY_SSH_KEY_B64:-}' DEPLOY_LXC_PWD_B64='${DEPLOY_LXC_PWD_B64:-}' DEPLOY_LXC_WAN_BRIDGE='${DEPLOY_LXC_WAN_BRIDGE:-}' DEPLOY_LXC_WAN_IP='${DEPLOY_LXC_WAN_IP:-}' DEPLOY_LXC_LAN_BRIDGE='${DEPLOY_LXC_LAN_BRIDGE:-}' DEPLOY_LXC_LAN_SUBNET='${DEPLOY_LXC_LAN_SUBNET:-}' DEPLOY_LXC_NET1='${DEPLOY_LXC_NET1:-}'" bash -s << 'REMOTE' set -e DEPLOY=/tmp/emmc-provisioning-deploy ROOTFS_STORAGE="${ROOTFS_STORAGE:?ROOTFS_STORAGE not set}" @@ -185,14 +191,24 @@ else fi [[ -z "$DEBIAN12_TMPL" ]] && { log "Error: no Debian 12 template found"; exit 1; } TMPL_NAME=$(basename "$DEBIAN12_TMPL") - # Optional: add eth1 for network-boot LAN (DHCP+TFTP). Set DEPLOY_LXC_NET1 e.g. "name=eth1,bridge=vmbr1,ip=10.20.50.1/24" + # WAN (eth0): bridge and IP from env; default vmbr0 + dhcp + WAN_BRIDGE="${DEPLOY_LXC_WAN_BRIDGE:-vmbr0}" + WAN_IP="${DEPLOY_LXC_WAN_IP:-dhcp}" + # LAN (eth1): optional; use DEPLOY_LXC_LAN_BRIDGE + DEPLOY_LXC_LAN_SUBNET, or legacy DEPLOY_LXC_NET1 NET1_OPT="" - if [[ -n "${DEPLOY_LXC_NET1:-}" ]]; then + if [[ -n "${DEPLOY_LXC_LAN_BRIDGE:-}" ]]; then + LAN_SUBNET="${DEPLOY_LXC_LAN_SUBNET:-10.20.50.1/24}" + NET1_OPT="--net1 name=eth1,bridge=${DEPLOY_LXC_LAN_BRIDGE},ip=${LAN_SUBNET}" + log "LXC network: eth0 WAN bridge=$WAN_BRIDGE ip=$WAN_IP; eth1 LAN bridge=$DEPLOY_LXC_LAN_BRIDGE ip=$LAN_SUBNET" + elif [[ -n "${DEPLOY_LXC_NET1:-}" ]]; then NET1_OPT="--net1 $DEPLOY_LXC_NET1" + log "LXC network: eth0 WAN bridge=$WAN_BRIDGE ip=$WAN_IP; eth1 from DEPLOY_LXC_NET1" + else + log "LXC network: eth0 WAN bridge=$WAN_BRIDGE ip=$WAN_IP (no LAN interface)" fi pct create "$CTID" "local:vztmpl/${TMPL_NAME}" \ --hostname "$LXC_HOSTNAME" --memory 1024 --swap 0 --cores 1 \ - --rootfs "${ROOTFS_STORAGE}:8" --net0 name=eth0,bridge=vmbr0,ip=dhcp $NET1_OPT \ + --rootfs "${ROOTFS_STORAGE}:8" --net0 name=eth0,bridge="$WAN_BRIDGE",ip="$WAN_IP" $NET1_OPT \ --unprivileged 0 --features nesting=1 -tag cm4-provisioning mkdir -p /var/lib/cm4-provisioning pct set "$CTID" -mp0 /var/lib/cm4-provisioning,mp=/var/lib/cm4-provisioning @@ -302,6 +318,24 @@ fi log "Starting LXC $CTID if stopped ..." pct start "$CTID" 2>/dev/null || true +# --- LXC: write lan-subnet.conf when LAN bridge/subnet is set (so dnsmasq/NAT/toggle use same subnet) --- +LAN_SUBNET_FOR_CONF="${DEPLOY_LXC_LAN_SUBNET:-}" +[[ -z "$LAN_SUBNET_FOR_CONF" && -n "${DEPLOY_LXC_LAN_BRIDGE:-}" ]] && LAN_SUBNET_FOR_CONF="10.20.50.1/24" +if [[ -n "$LAN_SUBNET_FOR_CONF" ]]; then + if [[ "$LAN_SUBNET_FOR_CONF" =~ ^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/([0-9]+)$ ]]; then + LAN_GW="${BASH_REMATCH[1]}" + PREFIX="${BASH_REMATCH[2]}" + BASE_3="${LAN_GW%.*}" + LAN_CIDR="${BASE_3}.0/${PREFIX}" + DHCP_RANGE_START="${BASE_3}.100" + DHCP_RANGE_END="${BASE_3}.200" + pct exec "$CTID" -- bash -c "mkdir -p /opt/cm4-provisioning && echo 'LAN_GW=$LAN_GW' > /opt/cm4-provisioning/lan-subnet.conf && echo 'LAN_CIDR=$LAN_CIDR' >> /opt/cm4-provisioning/lan-subnet.conf && echo 'DHCP_RANGE_START=$DHCP_RANGE_START' >> /opt/cm4-provisioning/lan-subnet.conf && echo 'DHCP_RANGE_END=$DHCP_RANGE_END' >> /opt/cm4-provisioning/lan-subnet.conf" + log "LXC: wrote /opt/cm4-provisioning/lan-subnet.conf (LAN_GW=$LAN_GW, LAN_CIDR=$LAN_CIDR, DHCP ${DHCP_RANGE_START}-${DHCP_RANGE_END})" + else + log "Warning: DEPLOY_LXC_LAN_SUBNET=$LAN_SUBNET_FOR_CONF not in form A.B.C.D/PREFIX; skipping lan-subnet.conf" + fi +fi + # --- LXC: flash scripts (for reference; actual flash runs on host) --- log "LXC: installing flash scripts ..." pct exec "$CTID" -- mkdir -p /opt/cm4-provisioning /etc/cm4-provisioning diff --git a/emmc-provisioning/scripts/edit-cloudinit-on-image.sh b/emmc-provisioning/scripts/edit-cloudinit-on-image.sh new file mode 100755 index 0000000..d269de9 --- /dev/null +++ b/emmc-provisioning/scripts/edit-cloudinit-on-image.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +# Mount a Raspberry Pi OS .img or .img.xz, edit cloud-init NoCloud files on the boot +# partition, then unmount and (if .img.xz) recompress. Requires sudo for loop/mount. +# +# Usage: +# ./edit-cloudinit-on-image.sh +# ./edit-cloudinit-on-image.sh +# +# Options: +# --no-recompress If image was .img.xz, leave decompressed .img and do not overwrite +# --replace-with-repo Copy user-data, meta-data, network-config from repo before editing +# +# Example: +# ./edit-cloudinit-on-image.sh /path/to/gnss-bootstrap-20260223-215010.img.xz +# +# Backup the image before running if you want to keep the original. + +set -e + +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +CLOUDINIT_SRC="$REPO_ROOT/emmc-provisioning/cloud-init" +NO_RECOMPRESS="" +REPLACE_WITH_REPO="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --no-recompress) NO_RECOMPRESS=1; shift ;; + --replace-with-repo) REPLACE_WITH_REPO=1; shift ;; + -*) echo "Unknown option: $1"; exit 1 ;; + *) break ;; + esac +done + +IMAGE_IN="$1" +[[ -n "$IMAGE_IN" && -f "$IMAGE_IN" ]] || { + echo "Usage: $0 [--no-recompress] [--replace-with-repo] " + echo "Example: $0 gnss-bootstrap-20260223-215010.img.xz" + exit 1 +} + +IMAGE_IN="$(realpath "$IMAGE_IN")" +WORK_DIR="" +IMG_FILE="" +ORIGINAL_XZ="" +cleanup() { + if [[ -n "$MNT" && -d "$MNT" ]]; then + sudo umount "$MNT" 2>/dev/null || true + fi + if [[ -n "$LOOP" && -b "$LOOP" ]]; then + sudo losetup -d "$LOOP" 2>/dev/null || true + fi + if [[ -n "$WORK_DIR" && -d "$WORK_DIR" ]]; then + if [[ -n "$NO_RECOMPRESS" && -n "$ORIGINAL_XZ" ]]; then + echo "Work dir left at: $WORK_DIR" + else + rm -rf "$WORK_DIR" + fi + fi +} +trap cleanup EXIT + +if [[ "$IMAGE_IN" == *.img.xz ]]; then + ORIGINAL_XZ="$IMAGE_IN" + echo "Decompressing $(basename "$ORIGINAL_XZ") (this may take a minute and needs ~3GB free)…" + WORK_DIR=$(mktemp -d -p "${TMPDIR:-/tmp}" edit-cloudinit.XXXXXX) + IMG_FILE="$WORK_DIR/image.img" + xz -T 0 -d -k -f -c "$ORIGINAL_XZ" > "$IMG_FILE" +else + WORK_DIR=$(mktemp -d -p "${TMPDIR:-/tmp}" edit-cloudinit.XXXXXX) + IMG_FILE="$IMAGE_IN" +fi + +echo "Attaching image and mounting boot partition…" +LOOP=$(sudo losetup -f --show -P "$IMG_FILE") +# Force kernel to scan partition table so /dev/loopNp1, p2 etc. appear +sudo partprobe "$LOOP" 2>/dev/null || true +sleep 0.5 +boot_part="${LOOP}p1" +[[ -b "$boot_part" ]] || boot_part="${LOOP}p2" +[[ -b "$boot_part" ]] || { + echo "Boot partition not found on image. Partitions on image:" + ls -la "${LOOP}"p* 2>/dev/null || true + sudo fdisk -l "$IMG_FILE" 2>/dev/null || true + exit 1 +} + +MNT="$WORK_DIR/mnt" +mkdir -p "$MNT" +sudo mount "$boot_part" "$MNT" + +if [[ -n "$REPLACE_WITH_REPO" && -d "$CLOUDINIT_SRC" ]]; then + echo "Copying cloud-init files from repo into boot partition…" + for f in user-data meta-data network-config; do + if [[ -f "$CLOUDINIT_SRC/$f" ]]; then + sudo cp "$CLOUDINIT_SRC/$f" "$MNT/$f" + elif [[ -f "$CLOUDINIT_SRC/$f.bootstrap" ]]; then + sudo cp "$CLOUDINIT_SRC/$f.bootstrap" "$MNT/$f" + fi + done +fi + +echo "" +echo "Boot partition is mounted at: $MNT" +echo "Cloud-init files to edit:" +echo " $MNT/user-data" +echo " $MNT/meta-data" +echo " $MNT/network-config" +echo "" +EDITOR="${EDITOR:-nano}" +echo "Opening editor ($EDITOR). Save and exit when done." +read -r -p "Press Enter to open $EDITOR on these files…" +sudo "$EDITOR" "$MNT/user-data" "$MNT/meta-data" "$MNT/network-config" + +echo "Unmounting…" +sudo umount "$MNT" +MNT="" +sudo losetup -d "$LOOP" +LOOP="" + +if [[ -n "$ORIGINAL_XZ" && -z "$NO_RECOMPRESS" ]]; then + echo "Recompressing to $(basename "$ORIGINAL_XZ")…" + xz -T 0 -z -f -k "$IMG_FILE" + mv -f "${IMG_FILE}.xz" "$ORIGINAL_XZ" + echo "Done. Updated: $ORIGINAL_XZ" + rm -rf "$WORK_DIR" + WORK_DIR="" +elif [[ -n "$NO_RECOMPRESS" && -n "$ORIGINAL_XZ" ]]; then + echo "Left decompressed image at: $IMG_FILE" + echo "Recompress manually: xz -z -k \"$IMG_FILE\"" +fi diff --git a/emmc-provisioning/scripts/setup-network-boot-on-lxc.sh b/emmc-provisioning/scripts/setup-network-boot-on-lxc.sh index 78d91bf..cbd1280 100755 --- a/emmc-provisioning/scripts/setup-network-boot-on-lxc.sh +++ b/emmc-provisioning/scripts/setup-network-boot-on-lxc.sh @@ -25,7 +25,17 @@ if [[ -n "$TARGET" ]]; then fi # --- Running inside the LXC from here --- -echo "Configuring network boot (DHCP + TFTP on eth1, NAT via eth0) ..." +# LAN subnet: use /opt/cm4-provisioning/lan-subnet.conf (written by deploy-to-proxmox.sh when DEPLOY_LXC_LAN_SUBNET is set) +LAN_CONF="/opt/cm4-provisioning/lan-subnet.conf" +if [[ -f "$LAN_CONF" ]]; then + source "$LAN_CONF" +else + LAN_GW="10.20.50.1" + LAN_CIDR="10.20.50.0/24" + DHCP_RANGE_START="10.20.50.100" + DHCP_RANGE_END="10.20.50.200" +fi +echo "Configuring network boot (DHCP + TFTP on eth1, NAT via eth0) — LAN $LAN_CIDR (gateway $LAN_GW), DHCP ${DHCP_RANGE_START}-${DHCP_RANGE_END} ..." # 1) Install dnsmasq if ! command -v dnsmasq >/dev/null 2>&1; then @@ -34,12 +44,12 @@ fi # 2) dnsmasq config for eth1 only (DHCP + TFTP); PXE options in network-boot-pxe.conf (toggle with toggle-network-boot-dhcp.sh) mkdir -p /etc/dnsmasq.d -cat > /etc/dnsmasq.d/network-boot.conf << 'DNSMASQ' +cat > /etc/dnsmasq.d/network-boot.conf << DNSMASQ # DHCP on eth1 only (provisioning LAN) # TFTP and PXE options are in network-boot-pxe.conf, controlled by toggle-network-boot-dhcp.sh interface=eth1 bind-interfaces -dhcp-range=10.20.50.100,10.20.50.200,12h +dhcp-range=${DHCP_RANGE_START},${DHCP_RANGE_END},12h log-dhcp log-queries port=0 @@ -89,14 +99,14 @@ fi echo 'net.ipv4.ip_forward=1' > /etc/sysctl.d/99-cm4-network-boot.conf sysctl -p /etc/sysctl.d/99-cm4-network-boot.conf 2>/dev/null || sysctl -w net.ipv4.ip_forward=1 -# 5) NAT: 10.20.50.0/24 -> eth0 (masquerade) +# 5) NAT: LAN subnet -> eth0 (masquerade) if command -v nft >/dev/null 2>&1; then mkdir -p /etc/nftables.d - cat > /etc/nftables.d/nat-lan.conf << 'NFT' + cat > /etc/nftables.d/nat-lan.conf << NFT table ip nat { chain postrouting { type nat hook postrouting priority srcnat; policy accept; - ip saddr 10.20.50.0/24 oifname "eth0" masquerade + ip saddr ${LAN_CIDR} oifname "eth0" masquerade } } NFT @@ -110,8 +120,8 @@ NFT echo "NAT rule added (nftables) and saved to /etc/nftables.d/nat-lan.conf" else # Fallback iptables - iptables -t nat -C POSTROUTING -s 10.20.50.0/24 -o eth0 -j MASQUERADE 2>/dev/null || \ - iptables -t nat -A POSTROUTING -s 10.20.50.0/24 -o eth0 -j MASQUERADE + iptables -t nat -C POSTROUTING -s "${LAN_CIDR}" -o eth0 -j MASQUERADE 2>/dev/null || \ + iptables -t nat -A POSTROUTING -s "${LAN_CIDR}" -o eth0 -j MASQUERADE echo "NAT rule added (iptables)." fi @@ -120,6 +130,6 @@ systemctl enable dnsmasq systemctl restart dnsmasq echo "Network boot setup done." -echo " - DHCP + TFTP on eth1 (10.20.50.1), range 10.20.50.100-200" -echo " - NAT: 10.20.50.0/24 -> eth0 (internet)" +echo " - DHCP + TFTP on eth1 ($LAN_GW), range ${DHCP_RANGE_START}-${DHCP_RANGE_END}" +echo " - NAT: ${LAN_CIDR} -> eth0 (internet)" echo " - TFTP root: /srv/tftpboot (RPi boot files; initrd.img if provided)"