Initial Commit
This commit is contained in:
commit
ef98e3f131
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Build artifacts
|
||||||
|
*.iso
|
||||||
|
*.img
|
||||||
|
*.gz
|
||||||
|
*.tar
|
||||||
|
*.fat
|
||||||
|
|
||||||
|
# Seed working directories
|
||||||
|
seed-workdir-*/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Editor files
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Go binaries (flasher-tool)
|
||||||
|
flasher-tool
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
# incu-contrib
|
||||||
|
|
||||||
|
Peripheral tools, snippets, and scripts for working with [Incus](https://linuxcontainers.org/incus/),
|
||||||
|
[IncusOS](https://linuxcontainers.org/incus-os/), and the broader ecosystem.
|
||||||
|
|
||||||
|
Primarily aimed at home lab environments but useful in production as well.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
### [`incusos/`](incusos/)
|
||||||
|
|
||||||
|
Scripts for building IncusOS installation media and seed configurations:
|
||||||
|
|
||||||
|
- **`incusos-iso`** -- Build customized IncusOS ISO/IMG images using the official flasher-tool.
|
||||||
|
Supports both architectures (x86_64, aarch64), multiple applications (Incus, Operations Center),
|
||||||
|
automatic client certificate injection, and batch generation.
|
||||||
|
|
||||||
|
- **`incusos-seed`** -- Generate IncusOS seed archives (tar or FAT image) for installation
|
||||||
|
customization. Covers all seed file types: install, applications, Incus preseed,
|
||||||
|
Operations Center, network, and update configuration.
|
||||||
|
|
||||||
|
See [`incusos/README.md`](incusos/README.md) for full documentation.
|
||||||
|
|
||||||
|
### [`notes/`](notes/)
|
||||||
|
|
||||||
|
Research notes and reference material.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install the flasher-tool (requires Go 1.21+)
|
||||||
|
go install github.com/lxc/incus-os/incus-osd/cmd/flasher-tool@latest
|
||||||
|
|
||||||
|
# Build a default IncusOS ISO with your client cert injected
|
||||||
|
./incusos/incusos-iso
|
||||||
|
|
||||||
|
# Build for a specific architecture and application
|
||||||
|
./incusos/incusos-iso --arch aarch64 --app operations-center --defaults
|
||||||
|
|
||||||
|
# Generate all common ISO variants at once
|
||||||
|
./incusos/incusos-iso --batch
|
||||||
|
|
||||||
|
# Generate a standalone seed archive
|
||||||
|
./incusos/incusos-seed --app incus --defaults --output my-seed.tar
|
||||||
|
|
||||||
|
# Generate a SEED_DATA FAT image for external boot media
|
||||||
|
./incusos/incusos-seed --format fat --app incus --defaults --output seed.img
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- **Go 1.21+** -- for installing the flasher-tool
|
||||||
|
- **curl** -- for downloading images from the CDN
|
||||||
|
- **tar**, **gzip** -- for seed archive creation and image decompression
|
||||||
|
- **mtools** (optional) -- for creating FAT seed images
|
||||||
|
- **incus** (optional) -- for automatic client certificate detection
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
@ -0,0 +1,261 @@
|
||||||
|
# IncusOS Tools
|
||||||
|
|
||||||
|
Scripts for building IncusOS installation media and seed configurations.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
| Script | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| [`incusos-iso`](#incusos-iso) | Build customized IncusOS ISO/IMG images using the flasher-tool |
|
||||||
|
| [`incusos-seed`](#incusos-seed) | Generate seed archives (tar or FAT) for installation customization |
|
||||||
|
|
||||||
|
Both scripts support automatic client certificate injection from the local Incus
|
||||||
|
installation, so the freshly installed system is immediately manageable.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Go 1.21+** -- for installing the flasher-tool
|
||||||
|
- **flasher-tool** -- the official IncusOS image builder
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install github.com/lxc/incus-os/incus-osd/cmd/flasher-tool@latest
|
||||||
|
export PATH="${GOPATH:-$HOME/go}/bin:$PATH"
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
- **incus** -- for automatic client certificate detection
|
||||||
|
- **mtools** / **dosfstools** -- for creating FAT seed images
|
||||||
|
- **jq** or **python3** -- for parsing the CDN index (cross-architecture builds)
|
||||||
|
- **curl** -- for downloading images
|
||||||
|
|
||||||
|
## incusos-iso
|
||||||
|
|
||||||
|
Wrapper around the official flasher-tool that adds:
|
||||||
|
|
||||||
|
- **Cross-architecture builds** -- build aarch64 images on x86_64 and vice versa
|
||||||
|
- **Automatic seed generation** -- no need to manually create seed files
|
||||||
|
- **Client certificate injection** -- auto-detects your local Incus certificate
|
||||||
|
- **Batch mode** -- generate all common ISO variants in one command
|
||||||
|
- **Sensible defaults** -- minimal flags needed for common use cases
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build a default IncusOS ISO for the current architecture
|
||||||
|
./incusos-iso
|
||||||
|
|
||||||
|
# Incus with default configuration (ZFS pool, bridge, port 8443)
|
||||||
|
./incusos-iso --defaults
|
||||||
|
|
||||||
|
# Operations Center for aarch64
|
||||||
|
./incusos-iso --arch aarch64 --app operations-center --defaults
|
||||||
|
|
||||||
|
# Build a USB image instead of ISO
|
||||||
|
./incusos-iso --format img --defaults
|
||||||
|
|
||||||
|
# Generate all variants (2 arches x 2 apps x 2 default modes = 8 images)
|
||||||
|
./incusos-iso --batch
|
||||||
|
|
||||||
|
# Preview what would be built
|
||||||
|
./incusos-iso --batch --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options Reference
|
||||||
|
|
||||||
|
```
|
||||||
|
-a, --arch ARCH x86_64 or aarch64 (default: auto-detect)
|
||||||
|
-A, --app APP incus or operations-center (default: incus)
|
||||||
|
-d, --defaults Apply default config (ZFS pool, bridge, port 8443)
|
||||||
|
-c, --cert FILE Client certificate PEM file (default: auto-detect)
|
||||||
|
--no-cert Skip certificate injection
|
||||||
|
-F, --format FORMAT iso or img (default: iso)
|
||||||
|
-o, --output FILE Output filename (default: auto-generated)
|
||||||
|
-C, --channel CHANNEL stable or testing (default: stable)
|
||||||
|
-s, --seed FILE Use pre-built seed tar (skip seed generation)
|
||||||
|
-i, --image FILE Use local base image (skip CDN download)
|
||||||
|
-b, --batch Generate all common variants
|
||||||
|
--batch-arches LIST Architectures for batch (default: x86_64,aarch64)
|
||||||
|
--batch-apps LIST Apps for batch (default: incus,operations-center)
|
||||||
|
--output-dir DIR Output directory for batch (default: .)
|
||||||
|
--disk-target ID Target disk pattern (e.g. "virtio-*")
|
||||||
|
--force-install Overwrite existing installation
|
||||||
|
--force-reboot Auto-reboot after installation
|
||||||
|
--missing-tpm Allow install without TPM 2.0
|
||||||
|
--missing-secureboot Allow install without Secure Boot
|
||||||
|
--hostname NAME Set system hostname
|
||||||
|
--dry-run Show what would happen
|
||||||
|
-q, --quiet Suppress info output
|
||||||
|
-h, --help Full help text
|
||||||
|
-V, --version Show version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Output Naming Convention
|
||||||
|
|
||||||
|
Auto-generated filenames follow the pattern:
|
||||||
|
|
||||||
|
```
|
||||||
|
incusos-{app}-{defaults|bare}-{arch}.{iso|img}
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `incusos-incus-defaults-x86_64.iso`
|
||||||
|
- `incusos-ops-center-bare-aarch64.iso`
|
||||||
|
- `incusos-incus-defaults-x86_64.img`
|
||||||
|
|
||||||
|
### Batch Mode Matrix
|
||||||
|
|
||||||
|
With `--batch`, the following variants are generated:
|
||||||
|
|
||||||
|
| # | Architecture | Application | Defaults | Filename |
|
||||||
|
|---|-------------|-------------|----------|----------|
|
||||||
|
| 1 | x86_64 | Incus | yes | `incusos-incus-defaults-x86_64.iso` |
|
||||||
|
| 2 | x86_64 | Incus | no | `incusos-incus-bare-x86_64.iso` |
|
||||||
|
| 3 | x86_64 | Ops Center | yes | `incusos-ops-center-defaults-x86_64.iso` |
|
||||||
|
| 4 | x86_64 | Ops Center | no | `incusos-ops-center-bare-x86_64.iso` |
|
||||||
|
| 5 | aarch64 | Incus | yes | `incusos-incus-defaults-aarch64.iso` |
|
||||||
|
| 6 | aarch64 | Incus | no | `incusos-incus-bare-aarch64.iso` |
|
||||||
|
| 7 | aarch64 | Ops Center | yes | `incusos-ops-center-defaults-aarch64.iso` |
|
||||||
|
| 8 | aarch64 | Ops Center | no | `incusos-ops-center-bare-aarch64.iso` |
|
||||||
|
|
||||||
|
Customize with `--batch-arches` and `--batch-apps`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Only x86_64 Incus variants
|
||||||
|
./incusos-iso --batch --batch-arches x86_64 --batch-apps incus
|
||||||
|
```
|
||||||
|
|
||||||
|
## incusos-seed
|
||||||
|
|
||||||
|
Generates IncusOS seed archives containing the YAML configuration files
|
||||||
|
that customize an IncusOS installation. Output can be:
|
||||||
|
|
||||||
|
- **tar archive** (default) -- for use with `flasher-tool --seed-tar`
|
||||||
|
- **FAT image** -- for use as an external SEED_DATA partition/disk
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Standalone Incus with defaults and auto-detected certificate
|
||||||
|
./incusos-seed --defaults
|
||||||
|
|
||||||
|
# Operations Center seed
|
||||||
|
./incusos-seed --app operations-center --defaults --cert ~/.config/incus/client.crt
|
||||||
|
|
||||||
|
# Cluster node (no defaults) targeting virtio disks
|
||||||
|
./incusos-seed --disk-target 'virtio-*' --force-install --force-reboot
|
||||||
|
|
||||||
|
# FAT image for external boot media
|
||||||
|
./incusos-seed --format fat --defaults --output seed.img
|
||||||
|
|
||||||
|
# Preview generated YAML without creating files
|
||||||
|
./incusos-seed --defaults --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options Reference
|
||||||
|
|
||||||
|
```
|
||||||
|
-A, --app APP incus or operations-center (default: incus)
|
||||||
|
-d, --defaults Apply default configuration
|
||||||
|
-c, --cert FILE Client certificate PEM file (default: auto-detect)
|
||||||
|
--no-cert Skip certificate injection
|
||||||
|
-o, --output FILE Output file (default: incusos-seed.tar)
|
||||||
|
-f, --format FORMAT tar or fat (default: tar)
|
||||||
|
--fat-size SIZE FAT image size in MiB (default: 10)
|
||||||
|
--disk-target ID Target disk pattern
|
||||||
|
--force-install Overwrite existing installation
|
||||||
|
--force-reboot Auto-reboot after installation
|
||||||
|
--missing-tpm Allow install without TPM 2.0
|
||||||
|
--missing-secureboot Allow install without Secure Boot
|
||||||
|
--hostname NAME Set hostname
|
||||||
|
--domain NAME Set domain
|
||||||
|
--dns SERVERS Comma-separated DNS servers
|
||||||
|
--channel CHANNEL stable or testing (default: stable)
|
||||||
|
--auto-reboot Auto-reboot for updates
|
||||||
|
--dry-run Preview YAML output
|
||||||
|
-q, --quiet Suppress info output
|
||||||
|
-h, --help Full help text
|
||||||
|
-V, --version Show version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Seed Files Reference
|
||||||
|
|
||||||
|
The following YAML files are generated inside the archive:
|
||||||
|
|
||||||
|
| File | Always | Description |
|
||||||
|
|------|--------|-------------|
|
||||||
|
| `install.yaml` | yes | Installation target, security, reboot settings |
|
||||||
|
| `applications.yaml` | yes | Application selection |
|
||||||
|
| `incus.yaml` | when app=incus | Incus preseed (defaults, certificates) |
|
||||||
|
| `operations-center.yaml` | when app=ops-center | Operations Center config |
|
||||||
|
| `network.yaml` | when network opts set | Hostname, DNS, static IP |
|
||||||
|
| `update.yaml` | when non-default | Update channel, auto-reboot |
|
||||||
|
|
||||||
|
### Using the FAT Image (SEED_DATA)
|
||||||
|
|
||||||
|
The FAT output format creates a small disk image labeled `SEED_DATA` that can be
|
||||||
|
attached to a VM as a secondary disk. IncusOS detects this label at boot and
|
||||||
|
reads the seed configuration from it.
|
||||||
|
|
||||||
|
This is useful for the "pristine image" workflow:
|
||||||
|
|
||||||
|
1. Download one uncustomized IncusOS ISO
|
||||||
|
2. Generate per-node seed images with `incusos-seed --format fat`
|
||||||
|
3. Boot each VM with both the ISO and the node-specific seed image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate seeds for three cluster nodes
|
||||||
|
for i in 1 2 3; do
|
||||||
|
./incusos-seed --format fat \
|
||||||
|
--hostname "node-${i}" \
|
||||||
|
--disk-target 'virtio-*' \
|
||||||
|
--force-install \
|
||||||
|
--output "seed-node-${i}.img"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client Certificate Auto-Detection
|
||||||
|
|
||||||
|
Both scripts automatically look for a client certificate in this order:
|
||||||
|
|
||||||
|
1. `incus remote get-client-certificate` (if the `incus` CLI is installed)
|
||||||
|
2. `~/.config/incus/client.crt`
|
||||||
|
3. `~/snap/incus/common/config/client.crt`
|
||||||
|
4. `~/.config/lxc/client.crt`
|
||||||
|
|
||||||
|
Override with `--cert FILE` or disable with `--no-cert`.
|
||||||
|
|
||||||
|
### After Installation
|
||||||
|
|
||||||
|
With an injected certificate, the remote is already trusted:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
incus remote add my-server <ip-address>
|
||||||
|
incus remote list
|
||||||
|
```
|
||||||
|
|
||||||
|
For web UI access, export a PKCS#12 bundle and import it into your browser:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl pkcs12 -export \
|
||||||
|
-inkey ~/.config/incus/client.key \
|
||||||
|
-in ~/.config/incus/client.crt \
|
||||||
|
-out ~/.config/incus/client.pfx
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
See the [`examples/`](examples/) directory for sample seed configurations:
|
||||||
|
|
||||||
|
- [`incus-standalone.yaml`](examples/incus-standalone.yaml) -- Single Incus server with defaults
|
||||||
|
- [`incus-cluster-node.yaml`](examples/incus-cluster-node.yaml) -- Cluster node (no defaults)
|
||||||
|
- [`ops-center.yaml`](examples/ops-center.yaml) -- Operations Center
|
||||||
|
- [`network-static.yaml`](examples/network-static.yaml) -- Static network configuration
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- **TPM 2.0** and **Secure Boot** are required by default. Use `--missing-tpm` and
|
||||||
|
`--missing-secureboot` only for lab environments or VMs that lack these features.
|
||||||
|
- All storage pools are **automatically encrypted** by IncusOS.
|
||||||
|
- Retrieve encryption recovery keys after installation with:
|
||||||
|
`incus admin os system security show`
|
||||||
|
- Keep recovery keys safe -- they are needed if TPM state changes.
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Example: Incus cluster node (no defaults)
|
||||||
|
#
|
||||||
|
# For cluster nodes, apply_defaults should be false to avoid
|
||||||
|
# creating conflicting storage pools and network bridges.
|
||||||
|
# The cluster leader will push configuration to joining nodes.
|
||||||
|
#
|
||||||
|
# Usage with incusos-seed:
|
||||||
|
# incusos-seed --cert ~/.config/incus/client.crt --hostname node-02
|
||||||
|
#
|
||||||
|
# After installation, join the cluster with:
|
||||||
|
# incus admin init (on the node, choosing "join existing cluster")
|
||||||
|
|
||||||
|
# --- incus.yaml ---
|
||||||
|
apply_defaults: false
|
||||||
|
preseed:
|
||||||
|
server:
|
||||||
|
certificates:
|
||||||
|
- name: "my-workstation"
|
||||||
|
type: "client"
|
||||||
|
certificate: |
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
<paste your certificate from: incus remote get-client-certificate>
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
|
# --- network.yaml ---
|
||||||
|
# dns:
|
||||||
|
# hostname: "node-02"
|
||||||
|
# domain: "lab.local"
|
||||||
|
# nameservers:
|
||||||
|
# - "192.168.1.1"
|
||||||
|
# interfaces:
|
||||||
|
# - name: "mgmt"
|
||||||
|
# hwaddr: "enp5s0"
|
||||||
|
# addresses:
|
||||||
|
# - "dhcp4"
|
||||||
|
# roles:
|
||||||
|
# - management
|
||||||
|
# - cluster
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Example: Standalone Incus server with defaults
|
||||||
|
#
|
||||||
|
# This seed configures a single Incus server with:
|
||||||
|
# - ZFS storage pool "local" on remaining disk space
|
||||||
|
# - Network bridge "incusbr0"
|
||||||
|
# - Listening on port 8443 on all interfaces
|
||||||
|
# - Client certificate pre-trusted
|
||||||
|
#
|
||||||
|
# Usage with incusos-seed:
|
||||||
|
# incusos-seed --defaults --cert ~/.config/incus/client.crt
|
||||||
|
#
|
||||||
|
# Usage with flasher-tool (place files in a tar archive):
|
||||||
|
# tar -cf seed.tar install.yaml applications.yaml incus.yaml
|
||||||
|
# flasher-tool --seed-tar seed.tar
|
||||||
|
|
||||||
|
# --- install.yaml ---
|
||||||
|
# force_install: true
|
||||||
|
# force_reboot: true
|
||||||
|
# target:
|
||||||
|
# id: "virtio-*"
|
||||||
|
# security:
|
||||||
|
# missing_tpm: true
|
||||||
|
|
||||||
|
# --- applications.yaml ---
|
||||||
|
# applications:
|
||||||
|
# - name: incus
|
||||||
|
|
||||||
|
# --- incus.yaml ---
|
||||||
|
apply_defaults: true
|
||||||
|
preseed:
|
||||||
|
server:
|
||||||
|
certificates:
|
||||||
|
- name: "my-workstation"
|
||||||
|
type: "client"
|
||||||
|
certificate: |
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
<paste your certificate from: incus remote get-client-certificate>
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
# Example: Static network configuration
|
||||||
|
#
|
||||||
|
# IncusOS defaults to DHCP/SLAAC when no network seed is provided.
|
||||||
|
# Use this template for static IP configuration.
|
||||||
|
#
|
||||||
|
# This file goes into the seed archive as "network.yaml"
|
||||||
|
|
||||||
|
dns:
|
||||||
|
hostname: "incus-01"
|
||||||
|
domain: "lab.local"
|
||||||
|
nameservers:
|
||||||
|
- "192.168.1.1"
|
||||||
|
- "1.1.1.1"
|
||||||
|
search_domains:
|
||||||
|
- "lab.local"
|
||||||
|
|
||||||
|
time:
|
||||||
|
ntp_servers:
|
||||||
|
- "pool.ntp.org"
|
||||||
|
timezone: "America/New_York"
|
||||||
|
|
||||||
|
interfaces:
|
||||||
|
- name: "mgmt"
|
||||||
|
hwaddr: "enp5s0" # interface name or MAC address
|
||||||
|
addresses:
|
||||||
|
- "192.168.1.100/24"
|
||||||
|
roles:
|
||||||
|
- management
|
||||||
|
- cluster
|
||||||
|
mtu: 1500
|
||||||
|
routes:
|
||||||
|
- to: "0.0.0.0/0"
|
||||||
|
via: "192.168.1.1"
|
||||||
|
|
||||||
|
# --- Bonding example (uncomment to use) ---
|
||||||
|
# bonds:
|
||||||
|
# - name: "bond0"
|
||||||
|
# members:
|
||||||
|
# - "enp5s0"
|
||||||
|
# - "enp6s0"
|
||||||
|
# mode: "802.3ad"
|
||||||
|
# addresses:
|
||||||
|
# - "dhcp4"
|
||||||
|
# roles:
|
||||||
|
# - management
|
||||||
|
|
||||||
|
# --- VLAN example (uncomment to use) ---
|
||||||
|
# vlans:
|
||||||
|
# - name: "vlan100"
|
||||||
|
# parent: "enp5s0"
|
||||||
|
# id: 100
|
||||||
|
# addresses:
|
||||||
|
# - "10.100.0.10/24"
|
||||||
|
# roles:
|
||||||
|
# - instances
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Example: Operations Center
|
||||||
|
#
|
||||||
|
# Operations Center provides centralized management for multiple IncusOS nodes.
|
||||||
|
# It can also serve as an update provider for air-gapped environments.
|
||||||
|
#
|
||||||
|
# IMPORTANT: At least one trusted client certificate must be provided,
|
||||||
|
# otherwise you will be locked out of the Operations Center after installation.
|
||||||
|
#
|
||||||
|
# Usage with incusos-seed:
|
||||||
|
# incusos-seed --app operations-center --defaults --cert ~/.config/incus/client.crt
|
||||||
|
#
|
||||||
|
# Usage with incusos-iso:
|
||||||
|
# incusos-iso --app operations-center --defaults
|
||||||
|
|
||||||
|
# --- operations-center.yaml ---
|
||||||
|
apply_defaults: true
|
||||||
|
trusted_client_certificates:
|
||||||
|
- |
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
<paste your certificate from: incus remote get-client-certificate>
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
|
# --- network.yaml (optional) ---
|
||||||
|
# dns:
|
||||||
|
# hostname: "ops-center"
|
||||||
|
# domain: "lab.local"
|
||||||
|
|
||||||
|
# --- update.yaml (optional) ---
|
||||||
|
# channel: "stable"
|
||||||
|
# auto_reboot: false
|
||||||
|
# check_frequency: "6h"
|
||||||
|
|
@ -0,0 +1,946 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# incusos-iso - Build IncusOS ISO/IMG images using the official flasher-tool
|
||||||
|
#
|
||||||
|
# Wrapper around the IncusOS flasher-tool that adds cross-architecture support,
|
||||||
|
# automatic seed generation, client certificate injection, and batch builds.
|
||||||
|
#
|
||||||
|
# Usage: incusos-iso [OPTIONS]
|
||||||
|
# Run 'incusos-iso --help' for full usage information.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
readonly VERSION="0.1.0"
|
||||||
|
readonly SCRIPT_NAME="incusos-iso"
|
||||||
|
readonly CDN_INDEX="https://images.linuxcontainers.org/os/index.json"
|
||||||
|
readonly CDN_BASE="https://images.linuxcontainers.org/os"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Color and formatting
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
setup_colors() {
|
||||||
|
if [[ -t 1 ]] && [[ "${NO_COLOR:-}" != "1" ]] && [[ "${TERM:-}" != "dumb" ]]; then
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
DIM='\033[2m'
|
||||||
|
RESET='\033[0m'
|
||||||
|
else
|
||||||
|
RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' DIM='' RESET=''
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
info() { echo -e "${BLUE}[info]${RESET} $*"; }
|
||||||
|
success() { echo -e "${GREEN}[ok]${RESET} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[warn]${RESET} $*" >&2; }
|
||||||
|
error() { echo -e "${RED}[error]${RESET} $*" >&2; }
|
||||||
|
step() { echo -e "${CYAN}[step]${RESET} ${BOLD}$*${RESET}"; }
|
||||||
|
detail() { echo -e "${DIM} $*${RESET}"; }
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Help
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
${BOLD}${SCRIPT_NAME}${RESET} v${VERSION} - Build IncusOS ISO/IMG images
|
||||||
|
|
||||||
|
${BOLD}USAGE${RESET}
|
||||||
|
${SCRIPT_NAME} [OPTIONS]
|
||||||
|
|
||||||
|
${BOLD}OPTIONS${RESET}
|
||||||
|
${BOLD}-a, --arch${RESET} ARCH Target architecture: ${CYAN}x86_64${RESET}, ${CYAN}aarch64${RESET}
|
||||||
|
(default: auto-detect current system)
|
||||||
|
${BOLD}-A, --app${RESET} APP Application: ${CYAN}incus${RESET}, ${CYAN}operations-center${RESET}
|
||||||
|
(default: incus)
|
||||||
|
${BOLD}-d, --defaults${RESET} Apply default configuration (ZFS pool, bridge, port 8443)
|
||||||
|
${BOLD}-c, --cert${RESET} FILE Path to client certificate PEM file
|
||||||
|
(default: auto-detect from local Incus installation)
|
||||||
|
${BOLD}--no-cert${RESET} Do not inject any client certificate
|
||||||
|
${BOLD}-F, --format${RESET} FORMAT Image format: ${CYAN}iso${RESET}, ${CYAN}img${RESET} (default: iso)
|
||||||
|
${BOLD}-o, --output${RESET} FILE Output filename (default: auto-generated)
|
||||||
|
${BOLD}-C, --channel${RESET} CHANNEL Update channel: ${CYAN}stable${RESET}, ${CYAN}testing${RESET} (default: stable)
|
||||||
|
|
||||||
|
${BOLD}Seed options:${RESET}
|
||||||
|
${BOLD}-s, --seed${RESET} FILE Use a pre-built seed tar archive (skip seed generation)
|
||||||
|
${BOLD}--disk-target${RESET} ID Target disk ID pattern (e.g. "virtio-*")
|
||||||
|
${BOLD}--force-install${RESET} Overwrite existing installation without prompting
|
||||||
|
${BOLD}--force-reboot${RESET} Automatically reboot after installation
|
||||||
|
${BOLD}--missing-tpm${RESET} Allow installation without TPM 2.0
|
||||||
|
${BOLD}--missing-secureboot${RESET} Allow installation without Secure Boot
|
||||||
|
${BOLD}--hostname${RESET} NAME Set system hostname
|
||||||
|
|
||||||
|
${BOLD}Image source:${RESET}
|
||||||
|
${BOLD}-i, --image${RESET} FILE Use a local base image (skip CDN download)
|
||||||
|
|
||||||
|
${BOLD}Batch mode:${RESET}
|
||||||
|
${BOLD}-b, --batch${RESET} Generate all common ISO variants
|
||||||
|
${BOLD}--batch-arches${RESET} LIST Architectures for batch mode (default: "x86_64,aarch64")
|
||||||
|
${BOLD}--batch-apps${RESET} LIST Applications for batch mode (default: "incus,operations-center")
|
||||||
|
${BOLD}--output-dir${RESET} DIR Output directory for batch mode (default: current directory)
|
||||||
|
|
||||||
|
${BOLD}General:${RESET}
|
||||||
|
${BOLD}--dry-run${RESET} Show what would be done without executing
|
||||||
|
${BOLD}-q, --quiet${RESET} Suppress informational output
|
||||||
|
${BOLD}-h, --help${RESET} Show this help message
|
||||||
|
${BOLD}-V, --version${RESET} Show version
|
||||||
|
|
||||||
|
${BOLD}EXAMPLES${RESET}
|
||||||
|
# Build default ISO for current architecture (Incus, no defaults)
|
||||||
|
${SCRIPT_NAME}
|
||||||
|
|
||||||
|
# Incus with defaults, for x86_64
|
||||||
|
${SCRIPT_NAME} --arch x86_64 --defaults
|
||||||
|
|
||||||
|
# Operations Center for aarch64
|
||||||
|
${SCRIPT_NAME} --arch aarch64 --app operations-center --defaults
|
||||||
|
|
||||||
|
# Use a pre-built seed archive
|
||||||
|
${SCRIPT_NAME} --seed my-config.tar --arch x86_64
|
||||||
|
|
||||||
|
# Build USB image instead of ISO
|
||||||
|
${SCRIPT_NAME} --format img --defaults
|
||||||
|
|
||||||
|
# Generate all common variants
|
||||||
|
${SCRIPT_NAME} --batch
|
||||||
|
|
||||||
|
# Preview what would be built
|
||||||
|
${SCRIPT_NAME} --batch --dry-run
|
||||||
|
|
||||||
|
${BOLD}PREREQUISITES${RESET}
|
||||||
|
The flasher-tool must be installed. Install with:
|
||||||
|
go install github.com/lxc/incus-os/incus-osd/cmd/flasher-tool@latest
|
||||||
|
|
||||||
|
Ensure \$GOPATH/bin (typically ~/go/bin) is in your PATH.
|
||||||
|
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Argument parsing
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Defaults
|
||||||
|
ARCH=""
|
||||||
|
APP="incus"
|
||||||
|
APPLY_DEFAULTS=false
|
||||||
|
CERT_FILE=""
|
||||||
|
NO_CERT=false
|
||||||
|
IMG_FORMAT="iso"
|
||||||
|
OUTPUT=""
|
||||||
|
CHANNEL="stable"
|
||||||
|
SEED_FILE=""
|
||||||
|
LOCAL_IMAGE=""
|
||||||
|
DISK_TARGET=""
|
||||||
|
FORCE_INSTALL=false
|
||||||
|
FORCE_REBOOT=false
|
||||||
|
MISSING_TPM=false
|
||||||
|
MISSING_SECUREBOOT=false
|
||||||
|
HOSTNAME_OPT=""
|
||||||
|
BATCH=false
|
||||||
|
BATCH_ARCHES="x86_64,aarch64"
|
||||||
|
BATCH_APPS="incus,operations-center"
|
||||||
|
OUTPUT_DIR="."
|
||||||
|
DRY_RUN=false
|
||||||
|
QUIET=false
|
||||||
|
|
||||||
|
parse_args() {
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-a|--arch)
|
||||||
|
ARCH="${2:?'--arch requires a value'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-A|--app)
|
||||||
|
APP="${2:?'--app requires a value'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-d|--defaults)
|
||||||
|
APPLY_DEFAULTS=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-c|--cert)
|
||||||
|
CERT_FILE="${2:?'--cert requires a file path'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--no-cert)
|
||||||
|
NO_CERT=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-F|--format)
|
||||||
|
IMG_FORMAT="${2:?'--format requires a value'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-o|--output)
|
||||||
|
OUTPUT="${2:?'--output requires a file path'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-C|--channel)
|
||||||
|
CHANNEL="${2:?'--channel requires a value'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-s|--seed)
|
||||||
|
SEED_FILE="${2:?'--seed requires a file path'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-i|--image)
|
||||||
|
LOCAL_IMAGE="${2:?'--image requires a file path'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--disk-target)
|
||||||
|
DISK_TARGET="${2:?'--disk-target requires a value'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--force-install)
|
||||||
|
FORCE_INSTALL=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--force-reboot)
|
||||||
|
FORCE_REBOOT=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--missing-tpm)
|
||||||
|
MISSING_TPM=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--missing-secureboot)
|
||||||
|
MISSING_SECUREBOOT=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--hostname)
|
||||||
|
HOSTNAME_OPT="${2:?'--hostname requires a value'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-b|--batch)
|
||||||
|
BATCH=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--batch-arches)
|
||||||
|
BATCH_ARCHES="${2:?'--batch-arches requires a value'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--batch-apps)
|
||||||
|
BATCH_APPS="${2:?'--batch-apps requires a value'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--output-dir)
|
||||||
|
OUTPUT_DIR="${2:?'--output-dir requires a value'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--dry-run)
|
||||||
|
DRY_RUN=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-q|--quiet)
|
||||||
|
QUIET=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
-V|--version)
|
||||||
|
echo "${SCRIPT_NAME} v${VERSION}"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
error "Unknown option: $1"
|
||||||
|
echo "Run '${SCRIPT_NAME} --help' for usage information." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
error "Unexpected argument: $1"
|
||||||
|
echo "Run '${SCRIPT_NAME} --help' for usage information." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Validation
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
detect_arch() {
|
||||||
|
local machine
|
||||||
|
machine=$(uname -m)
|
||||||
|
case "$machine" in
|
||||||
|
x86_64) echo "x86_64" ;;
|
||||||
|
aarch64) echo "aarch64" ;;
|
||||||
|
arm64) echo "aarch64" ;;
|
||||||
|
*)
|
||||||
|
error "Unsupported architecture: ${machine}"
|
||||||
|
error "IncusOS supports x86_64 and aarch64 only"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_args() {
|
||||||
|
# Auto-detect architecture if not specified
|
||||||
|
if [[ -z "$ARCH" ]]; then
|
||||||
|
ARCH=$(detect_arch)
|
||||||
|
[[ "$QUIET" != true ]] && info "Auto-detected architecture: ${ARCH}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$ARCH" in
|
||||||
|
x86_64|aarch64) ;;
|
||||||
|
*)
|
||||||
|
error "Invalid architecture: ${ARCH}"
|
||||||
|
error "Supported: x86_64, aarch64"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$APP" in
|
||||||
|
incus|operations-center) ;;
|
||||||
|
*)
|
||||||
|
error "Invalid application: ${APP}"
|
||||||
|
error "Supported: incus, operations-center"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$IMG_FORMAT" in
|
||||||
|
iso|img) ;;
|
||||||
|
*)
|
||||||
|
error "Invalid image format: ${IMG_FORMAT}"
|
||||||
|
error "Supported: iso, img"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$CHANNEL" in
|
||||||
|
stable|testing) ;;
|
||||||
|
*)
|
||||||
|
error "Invalid channel: ${CHANNEL}"
|
||||||
|
error "Supported: stable, testing"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ -n "$CERT_FILE" ]] && [[ "$NO_CERT" == true ]]; then
|
||||||
|
error "Cannot use both --cert and --no-cert"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$CERT_FILE" ]] && [[ ! -f "$CERT_FILE" ]]; then
|
||||||
|
error "Certificate file not found: ${CERT_FILE}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$SEED_FILE" ]] && [[ ! -f "$SEED_FILE" ]]; then
|
||||||
|
error "Seed file not found: ${SEED_FILE}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$LOCAL_IMAGE" ]] && [[ ! -f "$LOCAL_IMAGE" ]]; then
|
||||||
|
error "Local image not found: ${LOCAL_IMAGE}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Dependency checks
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
FLASHER_TOOL=""
|
||||||
|
|
||||||
|
find_flasher_tool() {
|
||||||
|
# Check common locations
|
||||||
|
local candidates=(
|
||||||
|
"flasher-tool"
|
||||||
|
"${GOPATH:-${HOME}/go}/bin/flasher-tool"
|
||||||
|
"${HOME}/go/bin/flasher-tool"
|
||||||
|
"/usr/local/bin/flasher-tool"
|
||||||
|
)
|
||||||
|
|
||||||
|
for candidate in "${candidates[@]}"; do
|
||||||
|
if command -v "$candidate" &>/dev/null; then
|
||||||
|
FLASHER_TOOL="$candidate"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ -x "$candidate" ]]; then
|
||||||
|
FLASHER_TOOL="$candidate"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
check_dependencies() {
|
||||||
|
step "Checking dependencies"
|
||||||
|
|
||||||
|
# flasher-tool
|
||||||
|
find_flasher_tool
|
||||||
|
if [[ -z "$FLASHER_TOOL" ]]; then
|
||||||
|
error "flasher-tool not found"
|
||||||
|
echo "" >&2
|
||||||
|
echo " Install the flasher-tool with:" >&2
|
||||||
|
echo " go install github.com/lxc/incus-os/incus-osd/cmd/flasher-tool@latest" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo " Make sure \$GOPATH/bin is in your PATH:" >&2
|
||||||
|
echo " export PATH=\"\${GOPATH:-\$HOME/go}/bin:\$PATH\"" >&2
|
||||||
|
echo "" >&2
|
||||||
|
|
||||||
|
# Offer to install if Go is available
|
||||||
|
if command -v go &>/dev/null; then
|
||||||
|
echo -n " Go is installed. Install flasher-tool now? [Y/n] " >&2
|
||||||
|
local reply
|
||||||
|
read -r reply
|
||||||
|
if [[ "$reply" =~ ^[Yy]?$ ]]; then
|
||||||
|
install_flasher_tool
|
||||||
|
else
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
error "Go is not installed. Install Go 1.21+ first, then the flasher-tool."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
success "flasher-tool: ${FLASHER_TOOL}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# curl (needed for cross-arch downloads)
|
||||||
|
if ! command -v curl &>/dev/null; then
|
||||||
|
warn "curl not found -- cross-architecture downloads will not be available"
|
||||||
|
else
|
||||||
|
detail "curl: $(command -v curl)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# gzip
|
||||||
|
if ! command -v gzip &>/dev/null; then
|
||||||
|
warn "gzip not found -- image decompression may fail"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# tar
|
||||||
|
if ! command -v tar &>/dev/null; then
|
||||||
|
error "tar is required but not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_flasher_tool() {
|
||||||
|
step "Installing flasher-tool"
|
||||||
|
info "Running: go install github.com/lxc/incus-os/incus-osd/cmd/flasher-tool@latest"
|
||||||
|
|
||||||
|
if go install github.com/lxc/incus-os/incus-osd/cmd/flasher-tool@latest; then
|
||||||
|
find_flasher_tool
|
||||||
|
if [[ -n "$FLASHER_TOOL" ]]; then
|
||||||
|
success "flasher-tool installed: ${FLASHER_TOOL}"
|
||||||
|
else
|
||||||
|
# Try adding GOPATH/bin to PATH for this session
|
||||||
|
export PATH="${GOPATH:-${HOME}/go}/bin:${PATH}"
|
||||||
|
find_flasher_tool
|
||||||
|
if [[ -n "$FLASHER_TOOL" ]]; then
|
||||||
|
success "flasher-tool installed: ${FLASHER_TOOL}"
|
||||||
|
else
|
||||||
|
error "flasher-tool was installed but could not be found in PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
error "Failed to install flasher-tool"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Certificate detection
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
CERT_CONTENT=""
|
||||||
|
|
||||||
|
detect_client_cert() {
|
||||||
|
if [[ "$NO_CERT" == true ]]; then
|
||||||
|
[[ "$QUIET" != true ]] && info "Skipping certificate injection (--no-cert)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$CERT_FILE" ]]; then
|
||||||
|
step "Loading client certificate from ${CERT_FILE}"
|
||||||
|
CERT_CONTENT=$(<"$CERT_FILE")
|
||||||
|
if [[ "$CERT_CONTENT" != *"BEGIN CERTIFICATE"* ]]; then
|
||||||
|
error "File does not appear to be a PEM certificate: ${CERT_FILE}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
success "Certificate loaded"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Auto-detection
|
||||||
|
step "Auto-detecting client certificate"
|
||||||
|
|
||||||
|
# Try incus CLI
|
||||||
|
if command -v incus &>/dev/null; then
|
||||||
|
local cert
|
||||||
|
if cert=$(incus remote get-client-certificate 2>/dev/null); then
|
||||||
|
CERT_CONTENT="$cert"
|
||||||
|
success "Certificate obtained from Incus CLI"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try common file locations
|
||||||
|
local cert_paths=(
|
||||||
|
"${HOME}/.config/incus/client.crt"
|
||||||
|
"${HOME}/snap/incus/common/config/client.crt"
|
||||||
|
"${HOME}/.config/lxc/client.crt"
|
||||||
|
)
|
||||||
|
|
||||||
|
for path in "${cert_paths[@]}"; do
|
||||||
|
if [[ -f "$path" ]]; then
|
||||||
|
CERT_CONTENT=$(<"$path")
|
||||||
|
success "Certificate loaded from ${path}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
warn "No client certificate found -- image will require manual certificate setup"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CDN operations
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fetch_latest_version() {
|
||||||
|
local channel="$1"
|
||||||
|
|
||||||
|
step "Fetching latest IncusOS version (channel: ${channel})"
|
||||||
|
|
||||||
|
local index_file
|
||||||
|
index_file=$(mktemp "${TMPDIR:-/tmp}/incusos-index-XXXXXX.json")
|
||||||
|
|
||||||
|
if ! curl -fsSL "$CDN_INDEX" -o "$index_file" 2>/dev/null; then
|
||||||
|
error "Failed to fetch CDN index from ${CDN_INDEX}"
|
||||||
|
rm -f "$index_file"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local version=""
|
||||||
|
|
||||||
|
# Parse with jq if available, otherwise use python3
|
||||||
|
if command -v jq &>/dev/null; then
|
||||||
|
version=$(jq -r \
|
||||||
|
--arg ch "$channel" \
|
||||||
|
'[.updates[] | select(.channels[] == $ch)] | sort_by(.version) | last | .version // empty' \
|
||||||
|
"$index_file")
|
||||||
|
elif command -v python3 &>/dev/null; then
|
||||||
|
version=$(python3 -c "
|
||||||
|
import json, sys
|
||||||
|
data = json.load(open('$index_file'))
|
||||||
|
versions = [u['version'] for u in data.get('updates', []) if '$channel' in u.get('channels', [])]
|
||||||
|
if versions:
|
||||||
|
print(sorted(versions)[-1])
|
||||||
|
" 2>/dev/null)
|
||||||
|
else
|
||||||
|
error "Either jq or python3 is required to parse the CDN index"
|
||||||
|
rm -f "$index_file"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$index_file"
|
||||||
|
|
||||||
|
if [[ -z "$version" ]]; then
|
||||||
|
error "No version found for channel '${channel}'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
success "Latest version: ${version}"
|
||||||
|
echo "$version"
|
||||||
|
}
|
||||||
|
|
||||||
|
download_image() {
|
||||||
|
local version="$1"
|
||||||
|
local arch="$2"
|
||||||
|
local format="$3"
|
||||||
|
local output_path="$4"
|
||||||
|
|
||||||
|
local url="${CDN_BASE}/${version}/${arch}/IncusOS_${version}.${format}.gz"
|
||||||
|
local gz_path="${output_path}.gz"
|
||||||
|
|
||||||
|
step "Downloading IncusOS image"
|
||||||
|
detail "URL: ${url}"
|
||||||
|
detail "Target: ${output_path}"
|
||||||
|
|
||||||
|
if ! curl -fL --progress-bar "$url" -o "$gz_path"; then
|
||||||
|
error "Failed to download image from ${url}"
|
||||||
|
rm -f "$gz_path"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Decompressing image"
|
||||||
|
if ! gzip -d "$gz_path"; then
|
||||||
|
error "Failed to decompress image"
|
||||||
|
rm -f "$gz_path"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
success "Image ready: ${output_path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Seed generation (inline, for when incusos-seed is not used separately)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
generate_seed_tar() {
|
||||||
|
local output_tar="$1"
|
||||||
|
local workdir
|
||||||
|
workdir=$(mktemp -d "${TMPDIR:-/tmp}/incusos-seed-XXXXXX")
|
||||||
|
|
||||||
|
step "Generating seed archive"
|
||||||
|
|
||||||
|
# install.yaml
|
||||||
|
{
|
||||||
|
echo "# Generated by ${SCRIPT_NAME} v${VERSION}"
|
||||||
|
[[ "$FORCE_INSTALL" == true ]] && echo "force_install: true"
|
||||||
|
[[ "$FORCE_REBOOT" == true ]] && echo "force_reboot: true"
|
||||||
|
if [[ -n "$DISK_TARGET" ]]; then
|
||||||
|
echo "target:"
|
||||||
|
echo " id: \"${DISK_TARGET}\""
|
||||||
|
fi
|
||||||
|
if [[ "$MISSING_TPM" == true ]] || [[ "$MISSING_SECUREBOOT" == true ]]; then
|
||||||
|
echo "security:"
|
||||||
|
[[ "$MISSING_TPM" == true ]] && echo " missing_tpm: true"
|
||||||
|
[[ "$MISSING_SECUREBOOT" == true ]] && echo " missing_secure_boot: true"
|
||||||
|
fi
|
||||||
|
} > "${workdir}/install.yaml"
|
||||||
|
detail "install.yaml"
|
||||||
|
|
||||||
|
# applications.yaml
|
||||||
|
{
|
||||||
|
echo "applications:"
|
||||||
|
echo " - name: ${APP}"
|
||||||
|
} > "${workdir}/applications.yaml"
|
||||||
|
detail "applications.yaml (${APP})"
|
||||||
|
|
||||||
|
# App-specific seed
|
||||||
|
case "$APP" in
|
||||||
|
incus)
|
||||||
|
{
|
||||||
|
echo "apply_defaults: ${APPLY_DEFAULTS}"
|
||||||
|
if [[ -n "$CERT_CONTENT" ]]; then
|
||||||
|
echo "preseed:"
|
||||||
|
echo " server:"
|
||||||
|
echo " certificates:"
|
||||||
|
echo " - name: \"auto-injected-client\""
|
||||||
|
echo " type: \"client\""
|
||||||
|
echo " certificate: |"
|
||||||
|
while IFS= read -r line; do
|
||||||
|
echo " ${line}"
|
||||||
|
done <<< "$CERT_CONTENT"
|
||||||
|
fi
|
||||||
|
} > "${workdir}/incus.yaml"
|
||||||
|
detail "incus.yaml (defaults=${APPLY_DEFAULTS})"
|
||||||
|
;;
|
||||||
|
operations-center)
|
||||||
|
{
|
||||||
|
echo "apply_defaults: ${APPLY_DEFAULTS}"
|
||||||
|
if [[ -n "$CERT_CONTENT" ]]; then
|
||||||
|
echo "trusted_client_certificates:"
|
||||||
|
echo " - |"
|
||||||
|
while IFS= read -r line; do
|
||||||
|
echo " ${line}"
|
||||||
|
done <<< "$CERT_CONTENT"
|
||||||
|
fi
|
||||||
|
} > "${workdir}/operations-center.yaml"
|
||||||
|
detail "operations-center.yaml (defaults=${APPLY_DEFAULTS})"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# network.yaml (optional)
|
||||||
|
if [[ -n "$HOSTNAME_OPT" ]]; then
|
||||||
|
{
|
||||||
|
echo "dns:"
|
||||||
|
echo " hostname: \"${HOSTNAME_OPT}\""
|
||||||
|
} > "${workdir}/network.yaml"
|
||||||
|
detail "network.yaml (hostname=${HOSTNAME_OPT})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# update.yaml (optional)
|
||||||
|
if [[ "$CHANNEL" != "stable" ]]; then
|
||||||
|
{
|
||||||
|
echo "channel: \"${CHANNEL}\""
|
||||||
|
echo "auto_reboot: false"
|
||||||
|
echo "check_frequency: \"6h\""
|
||||||
|
} > "${workdir}/update.yaml"
|
||||||
|
detail "update.yaml (channel=${CHANNEL})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create tar
|
||||||
|
tar -cf "$output_tar" -C "$workdir" .
|
||||||
|
rm -rf "$workdir"
|
||||||
|
|
||||||
|
success "Seed archive: ${output_tar}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# ISO/IMG building
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
generate_output_name() {
|
||||||
|
local arch="$1"
|
||||||
|
local app="$2"
|
||||||
|
local defaults="$3"
|
||||||
|
local format="$4"
|
||||||
|
local dir="$5"
|
||||||
|
|
||||||
|
local defaults_tag
|
||||||
|
if [[ "$defaults" == true ]]; then
|
||||||
|
defaults_tag="defaults"
|
||||||
|
else
|
||||||
|
defaults_tag="bare"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local app_tag
|
||||||
|
case "$app" in
|
||||||
|
incus) app_tag="incus" ;;
|
||||||
|
operations-center) app_tag="ops-center" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "${dir}/incusos-${app_tag}-${defaults_tag}-${arch}.${format}"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_single() {
|
||||||
|
local arch="$1"
|
||||||
|
local app="$2"
|
||||||
|
local defaults="$3"
|
||||||
|
local format="$4"
|
||||||
|
local output="$5"
|
||||||
|
local seed="$6"
|
||||||
|
local image="$7"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}Building: ${CYAN}${output}${RESET}"
|
||||||
|
echo -e " Arch: ${arch} App: ${app} Defaults: ${defaults} Format: ${format}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
info "Dry run -- skipping build"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local tmpdir
|
||||||
|
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/incusos-build-XXXXXX")
|
||||||
|
trap 'rm -rf "$tmpdir"' RETURN
|
||||||
|
|
||||||
|
# Generate seed if not provided
|
||||||
|
local seed_tar="$seed"
|
||||||
|
if [[ -z "$seed_tar" ]]; then
|
||||||
|
seed_tar="${tmpdir}/seed.tar"
|
||||||
|
|
||||||
|
# Save and set state for seed generation
|
||||||
|
local save_app="$APP" save_defaults="$APPLY_DEFAULTS"
|
||||||
|
APP="$app"
|
||||||
|
APPLY_DEFAULTS="$defaults"
|
||||||
|
|
||||||
|
generate_seed_tar "$seed_tar"
|
||||||
|
|
||||||
|
APP="$save_app"
|
||||||
|
APPLY_DEFAULTS="$save_defaults"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine image path
|
||||||
|
local image_path="$image"
|
||||||
|
local needs_download=false
|
||||||
|
|
||||||
|
if [[ -z "$image_path" ]]; then
|
||||||
|
# Check if we need a cross-architecture download
|
||||||
|
local current_arch
|
||||||
|
current_arch=$(detect_arch)
|
||||||
|
|
||||||
|
if [[ "$arch" != "$current_arch" ]]; then
|
||||||
|
# Cross-architecture: must download manually
|
||||||
|
needs_download=true
|
||||||
|
fi
|
||||||
|
# Same architecture: flasher-tool can download itself
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build flasher-tool command
|
||||||
|
local cmd=("$FLASHER_TOOL")
|
||||||
|
cmd+=(--format "$format")
|
||||||
|
cmd+=(--seed-tar "$seed_tar")
|
||||||
|
cmd+=(--channel "$CHANNEL")
|
||||||
|
|
||||||
|
if [[ "$needs_download" == true ]]; then
|
||||||
|
# Download cross-arch image
|
||||||
|
local version
|
||||||
|
version=$(fetch_latest_version "$CHANNEL" | tail -1)
|
||||||
|
image_path="${tmpdir}/IncusOS_${version}.${format}"
|
||||||
|
download_image "$version" "$arch" "$format" "$image_path"
|
||||||
|
cmd+=(--image "$image_path")
|
||||||
|
elif [[ -n "$image_path" ]]; then
|
||||||
|
cmd+=(--image "$image_path")
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Running flasher-tool"
|
||||||
|
detail "${cmd[*]}"
|
||||||
|
|
||||||
|
# The flasher-tool writes to the image in place when --image is given,
|
||||||
|
# or downloads and writes when --image is not given.
|
||||||
|
# We need to handle the output file.
|
||||||
|
if [[ -n "$image_path" ]]; then
|
||||||
|
# flasher-tool modifies the image in place
|
||||||
|
"${cmd[@]}"
|
||||||
|
|
||||||
|
# Move to final output location
|
||||||
|
if [[ "$image_path" != "$output" ]]; then
|
||||||
|
mv "$image_path" "$output"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# flasher-tool will download and create output interactively
|
||||||
|
# We run it and let it produce the file, then rename
|
||||||
|
"${cmd[@]}"
|
||||||
|
|
||||||
|
# Find the produced file (flasher-tool outputs to current directory)
|
||||||
|
local produced
|
||||||
|
produced=$(ls -t IncusOS_*.${format} 2>/dev/null | head -1)
|
||||||
|
if [[ -n "$produced" ]] && [[ "$produced" != "$output" ]]; then
|
||||||
|
mv "$produced" "$output"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
success "Image built: ${output}"
|
||||||
|
|
||||||
|
local size
|
||||||
|
if [[ -f "$output" ]]; then
|
||||||
|
size=$(du -h "$output" | cut -f1)
|
||||||
|
detail "Size: ${size}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Batch mode
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
batch_build() {
|
||||||
|
IFS=',' read -ra arches <<< "$BATCH_ARCHES"
|
||||||
|
IFS=',' read -ra apps <<< "$BATCH_APPS"
|
||||||
|
|
||||||
|
local total=0
|
||||||
|
local variants=()
|
||||||
|
|
||||||
|
# Build matrix: arch x app x defaults(true/false)
|
||||||
|
for arch in "${arches[@]}"; do
|
||||||
|
for app in "${apps[@]}"; do
|
||||||
|
for defaults in true false; do
|
||||||
|
local outfile
|
||||||
|
outfile=$(generate_output_name "$arch" "$app" "$defaults" "$IMG_FORMAT" "$OUTPUT_DIR")
|
||||||
|
variants+=("${arch}|${app}|${defaults}|${outfile}")
|
||||||
|
total=$((total + 1))
|
||||||
|
done
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "${BOLD}Batch build: ${total} variants${RESET}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
local i=0
|
||||||
|
for variant in "${variants[@]}"; do
|
||||||
|
IFS='|' read -r arch app defaults outfile <<< "$variant"
|
||||||
|
i=$((i + 1))
|
||||||
|
echo -e "${BOLD}[${i}/${total}]${RESET} ${outfile}"
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
detail "arch=${arch} app=${app} defaults=${defaults} format=${IMG_FORMAT}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
build_single "$arch" "$app" "$defaults" "$IMG_FORMAT" "$outfile" "$SEED_FILE" "$LOCAL_IMAGE"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
info "Dry run complete -- no images were built"
|
||||||
|
else
|
||||||
|
success "Batch build complete: ${total} images generated"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Summary
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
print_summary() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}Build summary${RESET}"
|
||||||
|
echo -e " Architecture: ${CYAN}${ARCH}${RESET}"
|
||||||
|
echo -e " Application: ${CYAN}${APP}${RESET}"
|
||||||
|
echo -e " Defaults: $(if [[ "$APPLY_DEFAULTS" == true ]]; then echo "${GREEN}yes${RESET}"; else echo "${YELLOW}no${RESET}"; fi)"
|
||||||
|
echo -e " Certificate: $(if [[ -n "$CERT_CONTENT" ]]; then echo "${GREEN}injected${RESET}"; else echo "${DIM}none${RESET}"; fi)"
|
||||||
|
echo -e " Format: ${IMG_FORMAT}"
|
||||||
|
echo -e " Channel: ${CHANNEL}"
|
||||||
|
echo -e " Output: ${BOLD}${OUTPUT}${RESET}"
|
||||||
|
|
||||||
|
if [[ "$MISSING_TPM" == true ]]; then
|
||||||
|
echo -e " Security: ${YELLOW}TPM not required${RESET}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ -n "$CERT_CONTENT" ]]; then
|
||||||
|
echo -e "${DIM} The client certificate has been injected into the image.${RESET}"
|
||||||
|
echo -e "${DIM} After installation, add the remote with:${RESET}"
|
||||||
|
echo -e " incus remote add <name> <ip-address>"
|
||||||
|
else
|
||||||
|
echo -e "${DIM} No certificate was injected. After installation:${RESET}"
|
||||||
|
echo -e " incus remote add <name> <ip-address> --accept-certificate"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
main() {
|
||||||
|
setup_colors
|
||||||
|
parse_args "$@"
|
||||||
|
validate_args
|
||||||
|
|
||||||
|
if [[ "$QUIET" != true ]]; then
|
||||||
|
echo -e "${BOLD}${SCRIPT_NAME}${RESET} v${VERSION}"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" != true ]]; then
|
||||||
|
check_dependencies
|
||||||
|
else
|
||||||
|
find_flasher_tool
|
||||||
|
if [[ -z "$FLASHER_TOOL" ]]; then
|
||||||
|
FLASHER_TOOL="flasher-tool (not installed)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ "$BATCH" == true ]]; then
|
||||||
|
# In batch mode, cert detection applies to all builds
|
||||||
|
if [[ -z "$SEED_FILE" ]]; then
|
||||||
|
detect_client_cert
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
batch_build
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Single build mode
|
||||||
|
if [[ -z "$SEED_FILE" ]]; then
|
||||||
|
detect_client_cert
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set default output name
|
||||||
|
if [[ -z "$OUTPUT" ]]; then
|
||||||
|
OUTPUT=$(generate_output_name "$ARCH" "$APP" "$APPLY_DEFAULTS" "$IMG_FORMAT" "$OUTPUT_DIR")
|
||||||
|
fi
|
||||||
|
|
||||||
|
build_single "$ARCH" "$APP" "$APPLY_DEFAULTS" "$IMG_FORMAT" "$OUTPUT" "$SEED_FILE" "$LOCAL_IMAGE"
|
||||||
|
|
||||||
|
if [[ "$QUIET" != true ]] && [[ "$DRY_RUN" != true ]]; then
|
||||||
|
print_summary
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
|
|
@ -0,0 +1,706 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# incusos-seed - Generate IncusOS seed archives for installation customization
|
||||||
|
#
|
||||||
|
# Creates tar archives or FAT images containing seed configuration files
|
||||||
|
# for IncusOS installations. Supports all seed file types including install,
|
||||||
|
# applications, Incus preseed, Operations Center, network, and update config.
|
||||||
|
#
|
||||||
|
# Usage: incusos-seed [OPTIONS]
|
||||||
|
# Run 'incusos-seed --help' for full usage information.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
readonly VERSION="0.1.0"
|
||||||
|
readonly SCRIPT_NAME="incusos-seed"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Color and formatting
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
setup_colors() {
|
||||||
|
if [[ -t 1 ]] && [[ "${NO_COLOR:-}" != "1" ]] && [[ "${TERM:-}" != "dumb" ]]; then
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
DIM='\033[2m'
|
||||||
|
RESET='\033[0m'
|
||||||
|
else
|
||||||
|
RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' DIM='' RESET=''
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
info() { echo -e "${BLUE}[info]${RESET} $*"; }
|
||||||
|
success() { echo -e "${GREEN}[ok]${RESET} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[warn]${RESET} $*" >&2; }
|
||||||
|
error() { echo -e "${RED}[error]${RESET} $*" >&2; }
|
||||||
|
step() { echo -e "${CYAN}[step]${RESET} ${BOLD}$*${RESET}"; }
|
||||||
|
detail() { echo -e "${DIM} $*${RESET}"; }
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Help
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
${BOLD}${SCRIPT_NAME}${RESET} v${VERSION} - Generate IncusOS seed archives
|
||||||
|
|
||||||
|
${BOLD}USAGE${RESET}
|
||||||
|
${SCRIPT_NAME} [OPTIONS]
|
||||||
|
|
||||||
|
${BOLD}OPTIONS${RESET}
|
||||||
|
${BOLD}-A, --app${RESET} APP Application to configure: ${CYAN}incus${RESET}, ${CYAN}operations-center${RESET}
|
||||||
|
(default: incus)
|
||||||
|
${BOLD}-d, --defaults${RESET} Apply default configuration (ZFS pool, bridge, port 8443)
|
||||||
|
${BOLD}-c, --cert${RESET} FILE Path to client certificate PEM file
|
||||||
|
(default: auto-detect from local Incus installation)
|
||||||
|
${BOLD}--no-cert${RESET} Do not inject any client certificate
|
||||||
|
${BOLD}-o, --output${RESET} FILE Output file path (default: incusos-seed.tar)
|
||||||
|
|
||||||
|
${BOLD}Install options:${RESET}
|
||||||
|
${BOLD}--disk-target${RESET} ID Target disk ID pattern for /dev/disk/by-id/ (e.g. "virtio-*")
|
||||||
|
${BOLD}--force-install${RESET} Overwrite existing installation without prompting
|
||||||
|
${BOLD}--force-reboot${RESET} Automatically reboot after installation
|
||||||
|
${BOLD}--missing-tpm${RESET} Allow installation without TPM 2.0
|
||||||
|
${BOLD}--missing-secureboot${RESET} Allow installation without Secure Boot
|
||||||
|
|
||||||
|
${BOLD}Network options:${RESET}
|
||||||
|
${BOLD}--hostname${RESET} NAME Set system hostname
|
||||||
|
${BOLD}--domain${RESET} NAME Set system domain
|
||||||
|
${BOLD}--dns${RESET} SERVERS Comma-separated DNS servers (e.g. "8.8.8.8,1.1.1.1")
|
||||||
|
|
||||||
|
${BOLD}Update options:${RESET}
|
||||||
|
${BOLD}--channel${RESET} CHANNEL Update channel: ${CYAN}stable${RESET}, ${CYAN}testing${RESET} (default: stable)
|
||||||
|
${BOLD}--auto-reboot${RESET} Enable automatic reboot for updates
|
||||||
|
|
||||||
|
${BOLD}Output options:${RESET}
|
||||||
|
${BOLD}-f, --format${RESET} FORMAT Output format: ${CYAN}tar${RESET}, ${CYAN}fat${RESET} (default: tar)
|
||||||
|
tar: for use with flasher-tool --seed-tar
|
||||||
|
fat: FAT image with SEED_DATA label for external media
|
||||||
|
${BOLD}--fat-size${RESET} SIZE FAT image size in MiB (default: 10)
|
||||||
|
|
||||||
|
${BOLD}General:${RESET}
|
||||||
|
${BOLD}--dry-run${RESET} Show generated seed files without creating output
|
||||||
|
${BOLD}-q, --quiet${RESET} Suppress informational output
|
||||||
|
${BOLD}-h, --help${RESET} Show this help message
|
||||||
|
${BOLD}-V, --version${RESET} Show version
|
||||||
|
|
||||||
|
${BOLD}EXAMPLES${RESET}
|
||||||
|
# Standalone Incus with defaults and auto-detected certificate
|
||||||
|
${SCRIPT_NAME} --defaults
|
||||||
|
|
||||||
|
# Operations Center with explicit certificate
|
||||||
|
${SCRIPT_NAME} --app operations-center --defaults --cert ~/.config/incus/client.crt
|
||||||
|
|
||||||
|
# Cluster node (no defaults) with forced install on virtio disk
|
||||||
|
${SCRIPT_NAME} --disk-target 'virtio-*' --force-install --force-reboot
|
||||||
|
|
||||||
|
# Create a FAT image for SEED_DATA partition
|
||||||
|
${SCRIPT_NAME} --format fat --defaults --output seed.img
|
||||||
|
|
||||||
|
# Preview what would be generated
|
||||||
|
${SCRIPT_NAME} --defaults --dry-run
|
||||||
|
|
||||||
|
${BOLD}SEED FILES REFERENCE${RESET}
|
||||||
|
The following files are generated inside the archive:
|
||||||
|
|
||||||
|
${CYAN}install.yaml${RESET} Installation target, security, and reboot settings
|
||||||
|
${CYAN}applications.yaml${RESET} Application selection (incus, operations-center)
|
||||||
|
${CYAN}incus.yaml${RESET} Incus preseed (defaults, certificates)
|
||||||
|
${CYAN}operations-center.yaml${RESET} Operations Center config (when --app=operations-center)
|
||||||
|
${CYAN}network.yaml${RESET} Network configuration (hostname, DNS)
|
||||||
|
${CYAN}update.yaml${RESET} Update channel and reboot policy
|
||||||
|
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Argument parsing
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Defaults
|
||||||
|
APP="incus"
|
||||||
|
APPLY_DEFAULTS=false
|
||||||
|
CERT_FILE=""
|
||||||
|
NO_CERT=false
|
||||||
|
OUTPUT=""
|
||||||
|
DISK_TARGET=""
|
||||||
|
FORCE_INSTALL=false
|
||||||
|
FORCE_REBOOT=false
|
||||||
|
MISSING_TPM=false
|
||||||
|
MISSING_SECUREBOOT=false
|
||||||
|
HOSTNAME_OPT=""
|
||||||
|
DOMAIN_OPT=""
|
||||||
|
DNS_SERVERS=""
|
||||||
|
CHANNEL="stable"
|
||||||
|
AUTO_REBOOT=false
|
||||||
|
FORMAT="tar"
|
||||||
|
FAT_SIZE=10
|
||||||
|
DRY_RUN=false
|
||||||
|
QUIET=false
|
||||||
|
|
||||||
|
parse_args() {
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-A|--app)
|
||||||
|
APP="${2:?'--app requires a value'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-d|--defaults)
|
||||||
|
APPLY_DEFAULTS=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-c|--cert)
|
||||||
|
CERT_FILE="${2:?'--cert requires a file path'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--no-cert)
|
||||||
|
NO_CERT=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-o|--output)
|
||||||
|
OUTPUT="${2:?'--output requires a file path'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--disk-target)
|
||||||
|
DISK_TARGET="${2:?'--disk-target requires a value'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--force-install)
|
||||||
|
FORCE_INSTALL=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--force-reboot)
|
||||||
|
FORCE_REBOOT=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--missing-tpm)
|
||||||
|
MISSING_TPM=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--missing-secureboot)
|
||||||
|
MISSING_SECUREBOOT=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--hostname)
|
||||||
|
HOSTNAME_OPT="${2:?'--hostname requires a value'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--domain)
|
||||||
|
DOMAIN_OPT="${2:?'--domain requires a value'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--dns)
|
||||||
|
DNS_SERVERS="${2:?'--dns requires a value'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--channel)
|
||||||
|
CHANNEL="${2:?'--channel requires a value'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--auto-reboot)
|
||||||
|
AUTO_REBOOT=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-f|--format)
|
||||||
|
FORMAT="${2:?'--format requires a value'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--fat-size)
|
||||||
|
FAT_SIZE="${2:?'--fat-size requires a value'}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--dry-run)
|
||||||
|
DRY_RUN=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-q|--quiet)
|
||||||
|
QUIET=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
-V|--version)
|
||||||
|
echo "${SCRIPT_NAME} v${VERSION}"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
error "Unknown option: $1"
|
||||||
|
echo "Run '${SCRIPT_NAME} --help' for usage information." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
error "Unexpected argument: $1"
|
||||||
|
echo "Run '${SCRIPT_NAME} --help' for usage information." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Validation
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
validate_args() {
|
||||||
|
case "$APP" in
|
||||||
|
incus|operations-center) ;;
|
||||||
|
*)
|
||||||
|
error "Invalid application: ${APP}"
|
||||||
|
error "Supported: incus, operations-center"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$FORMAT" in
|
||||||
|
tar|fat) ;;
|
||||||
|
*)
|
||||||
|
error "Invalid output format: ${FORMAT}"
|
||||||
|
error "Supported: tar, fat"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$CHANNEL" in
|
||||||
|
stable|testing) ;;
|
||||||
|
*)
|
||||||
|
error "Invalid channel: ${CHANNEL}"
|
||||||
|
error "Supported: stable, testing"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ -n "$CERT_FILE" ]] && [[ "$NO_CERT" == true ]]; then
|
||||||
|
error "Cannot use both --cert and --no-cert"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$CERT_FILE" ]] && [[ ! -f "$CERT_FILE" ]]; then
|
||||||
|
error "Certificate file not found: ${CERT_FILE}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$FORMAT" == "fat" ]]; then
|
||||||
|
if ! command -v mcopy &>/dev/null && ! command -v mkfs.fat &>/dev/null; then
|
||||||
|
error "FAT image creation requires mtools (mcopy) or dosfstools (mkfs.fat)"
|
||||||
|
error "Install with: sudo apt install mtools dosfstools"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set default output filename
|
||||||
|
if [[ -z "$OUTPUT" ]]; then
|
||||||
|
case "$FORMAT" in
|
||||||
|
tar) OUTPUT="incusos-seed.tar" ;;
|
||||||
|
fat) OUTPUT="incusos-seed.img" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Certificate detection
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
CERT_CONTENT=""
|
||||||
|
|
||||||
|
detect_client_cert() {
|
||||||
|
if [[ "$NO_CERT" == true ]]; then
|
||||||
|
[[ "$QUIET" != true ]] && info "Skipping certificate injection (--no-cert)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$CERT_FILE" ]]; then
|
||||||
|
step "Loading client certificate from ${CERT_FILE}"
|
||||||
|
CERT_CONTENT=$(<"$CERT_FILE")
|
||||||
|
if [[ "$CERT_CONTENT" != *"BEGIN CERTIFICATE"* ]]; then
|
||||||
|
error "File does not appear to be a PEM certificate: ${CERT_FILE}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
success "Certificate loaded"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Auto-detection
|
||||||
|
step "Auto-detecting client certificate"
|
||||||
|
|
||||||
|
# Try incus CLI first
|
||||||
|
if command -v incus &>/dev/null; then
|
||||||
|
local cert
|
||||||
|
if cert=$(incus remote get-client-certificate 2>/dev/null); then
|
||||||
|
CERT_CONTENT="$cert"
|
||||||
|
success "Certificate obtained from Incus CLI"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
detail "Incus CLI available but could not retrieve certificate"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try common file locations
|
||||||
|
local cert_paths=(
|
||||||
|
"${HOME}/.config/incus/client.crt"
|
||||||
|
"${HOME}/snap/incus/common/config/client.crt"
|
||||||
|
"${HOME}/.config/lxc/client.crt"
|
||||||
|
)
|
||||||
|
|
||||||
|
for path in "${cert_paths[@]}"; do
|
||||||
|
if [[ -f "$path" ]]; then
|
||||||
|
CERT_CONTENT=$(<"$path")
|
||||||
|
success "Certificate loaded from ${path}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
warn "No client certificate found"
|
||||||
|
warn "The installed system will require manual certificate trust setup"
|
||||||
|
warn "Use --cert FILE to specify a certificate, or --no-cert to suppress this warning"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Seed file generation
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
WORKDIR=""
|
||||||
|
|
||||||
|
setup_workdir() {
|
||||||
|
WORKDIR=$(mktemp -d "${TMPDIR:-/tmp}/${SCRIPT_NAME}-XXXXXX")
|
||||||
|
trap 'rm -rf "$WORKDIR"' EXIT
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_install_yaml() {
|
||||||
|
local file="${WORKDIR}/install.yaml"
|
||||||
|
|
||||||
|
step "Generating install.yaml"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# IncusOS installation configuration"
|
||||||
|
echo "# Generated by ${SCRIPT_NAME} v${VERSION} on $(date -Iseconds)"
|
||||||
|
|
||||||
|
if [[ "$FORCE_INSTALL" == true ]]; then
|
||||||
|
echo "force_install: true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$FORCE_REBOOT" == true ]]; then
|
||||||
|
echo "force_reboot: true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$DISK_TARGET" ]]; then
|
||||||
|
echo "target:"
|
||||||
|
echo " id: \"${DISK_TARGET}\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$MISSING_TPM" == true ]] || [[ "$MISSING_SECUREBOOT" == true ]]; then
|
||||||
|
echo "security:"
|
||||||
|
if [[ "$MISSING_TPM" == true ]]; then
|
||||||
|
echo " missing_tpm: true"
|
||||||
|
fi
|
||||||
|
if [[ "$MISSING_SECUREBOOT" == true ]]; then
|
||||||
|
echo " missing_secure_boot: true"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
} > "$file"
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
detail "--- install.yaml ---"
|
||||||
|
while IFS= read -r line; do detail "$line"; done < "$file"
|
||||||
|
else
|
||||||
|
success "install.yaml"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_applications_yaml() {
|
||||||
|
local file="${WORKDIR}/applications.yaml"
|
||||||
|
|
||||||
|
step "Generating applications.yaml"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# IncusOS application selection"
|
||||||
|
echo "applications:"
|
||||||
|
echo " - name: ${APP}"
|
||||||
|
} > "$file"
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
detail "--- applications.yaml ---"
|
||||||
|
while IFS= read -r line; do detail "$line"; done < "$file"
|
||||||
|
else
|
||||||
|
success "applications.yaml (${APP})"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_incus_yaml() {
|
||||||
|
local file="${WORKDIR}/incus.yaml"
|
||||||
|
|
||||||
|
step "Generating incus.yaml"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Incus preseed configuration"
|
||||||
|
echo "apply_defaults: ${APPLY_DEFAULTS}"
|
||||||
|
|
||||||
|
if [[ -n "$CERT_CONTENT" ]]; then
|
||||||
|
echo "preseed:"
|
||||||
|
echo " server:"
|
||||||
|
echo " certificates:"
|
||||||
|
echo " - name: \"auto-injected-client\""
|
||||||
|
echo " type: \"client\""
|
||||||
|
echo " certificate: |"
|
||||||
|
# Indent each line of the certificate
|
||||||
|
while IFS= read -r line; do
|
||||||
|
echo " ${line}"
|
||||||
|
done <<< "$CERT_CONTENT"
|
||||||
|
fi
|
||||||
|
} > "$file"
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
detail "--- incus.yaml ---"
|
||||||
|
while IFS= read -r line; do detail "$line"; done < "$file"
|
||||||
|
else
|
||||||
|
local desc="defaults=$(bool_str $APPLY_DEFAULTS)"
|
||||||
|
[[ -n "$CERT_CONTENT" ]] && desc+=", certificate=injected"
|
||||||
|
success "incus.yaml (${desc})"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_ops_center_yaml() {
|
||||||
|
local file="${WORKDIR}/operations-center.yaml"
|
||||||
|
|
||||||
|
step "Generating operations-center.yaml"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Operations Center configuration"
|
||||||
|
echo "apply_defaults: ${APPLY_DEFAULTS}"
|
||||||
|
|
||||||
|
if [[ -n "$CERT_CONTENT" ]]; then
|
||||||
|
echo "trusted_client_certificates:"
|
||||||
|
echo " - |"
|
||||||
|
while IFS= read -r line; do
|
||||||
|
echo " ${line}"
|
||||||
|
done <<< "$CERT_CONTENT"
|
||||||
|
fi
|
||||||
|
} > "$file"
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
detail "--- operations-center.yaml ---"
|
||||||
|
while IFS= read -r line; do detail "$line"; done < "$file"
|
||||||
|
else
|
||||||
|
local desc="defaults=$(bool_str $APPLY_DEFAULTS)"
|
||||||
|
[[ -n "$CERT_CONTENT" ]] && desc+=", certificate=injected"
|
||||||
|
success "operations-center.yaml (${desc})"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_network_yaml() {
|
||||||
|
# Only generate if network options were specified
|
||||||
|
if [[ -z "$HOSTNAME_OPT" ]] && [[ -z "$DOMAIN_OPT" ]] && [[ -z "$DNS_SERVERS" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local file="${WORKDIR}/network.yaml"
|
||||||
|
|
||||||
|
step "Generating network.yaml"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Network configuration"
|
||||||
|
|
||||||
|
if [[ -n "$HOSTNAME_OPT" ]] || [[ -n "$DOMAIN_OPT" ]] || [[ -n "$DNS_SERVERS" ]]; then
|
||||||
|
echo "dns:"
|
||||||
|
[[ -n "$HOSTNAME_OPT" ]] && echo " hostname: \"${HOSTNAME_OPT}\""
|
||||||
|
[[ -n "$DOMAIN_OPT" ]] && echo " domain: \"${DOMAIN_OPT}\""
|
||||||
|
|
||||||
|
if [[ -n "$DNS_SERVERS" ]]; then
|
||||||
|
echo " nameservers:"
|
||||||
|
IFS=',' read -ra servers <<< "$DNS_SERVERS"
|
||||||
|
for srv in "${servers[@]}"; do
|
||||||
|
echo " - \"$(echo "$srv" | xargs)\""
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
} > "$file"
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
detail "--- network.yaml ---"
|
||||||
|
while IFS= read -r line; do detail "$line"; done < "$file"
|
||||||
|
else
|
||||||
|
local desc=""
|
||||||
|
[[ -n "$HOSTNAME_OPT" ]] && desc+="hostname=${HOSTNAME_OPT}"
|
||||||
|
[[ -n "$DNS_SERVERS" ]] && desc+=", dns=${DNS_SERVERS}"
|
||||||
|
success "network.yaml (${desc})"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_update_yaml() {
|
||||||
|
# Only generate if non-default values were specified
|
||||||
|
if [[ "$CHANNEL" == "stable" ]] && [[ "$AUTO_REBOOT" == false ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local file="${WORKDIR}/update.yaml"
|
||||||
|
|
||||||
|
step "Generating update.yaml"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Update configuration"
|
||||||
|
echo "channel: \"${CHANNEL}\""
|
||||||
|
echo "auto_reboot: ${AUTO_REBOOT}"
|
||||||
|
echo "check_frequency: \"6h\""
|
||||||
|
} > "$file"
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
detail "--- update.yaml ---"
|
||||||
|
while IFS= read -r line; do detail "$line"; done < "$file"
|
||||||
|
else
|
||||||
|
success "update.yaml (channel=${CHANNEL}, auto_reboot=$(bool_str $AUTO_REBOOT))"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Output creation
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
create_tar_output() {
|
||||||
|
step "Creating tar archive: ${OUTPUT}"
|
||||||
|
|
||||||
|
tar -cf "$OUTPUT" -C "$WORKDIR" .
|
||||||
|
|
||||||
|
local size
|
||||||
|
size=$(du -h "$OUTPUT" | cut -f1)
|
||||||
|
success "Seed archive created: ${OUTPUT} (${size})"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_fat_output() {
|
||||||
|
step "Creating FAT image: ${OUTPUT} (${FAT_SIZE} MiB)"
|
||||||
|
|
||||||
|
# Create empty image
|
||||||
|
dd if=/dev/zero of="$OUTPUT" bs=1M count="$FAT_SIZE" status=none
|
||||||
|
|
||||||
|
if command -v mkfs.fat &>/dev/null; then
|
||||||
|
mkfs.fat -n SEED_DATA "$OUTPUT" >/dev/null
|
||||||
|
|
||||||
|
if command -v mcopy &>/dev/null; then
|
||||||
|
# Use mtools to copy files without mounting
|
||||||
|
for f in "${WORKDIR}"/*.yaml; do
|
||||||
|
[[ -f "$f" ]] && mcopy -i "$OUTPUT" "$f" "::/$(basename "$f")"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
warn "mtools not installed -- FAT image created but files not copied"
|
||||||
|
warn "Install mtools and re-run, or manually mount and copy seed files"
|
||||||
|
warn "Files are in: ${WORKDIR}/"
|
||||||
|
# Keep workdir around
|
||||||
|
trap - EXIT
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
elif command -v mcopy &>/dev/null; then
|
||||||
|
# mtools can format too
|
||||||
|
mformat -i "$OUTPUT" -v SEED_DATA ::
|
||||||
|
for f in "${WORKDIR}"/*.yaml; do
|
||||||
|
[[ -f "$f" ]] && mcopy -i "$OUTPUT" "$f" "::/$(basename "$f")"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
local size
|
||||||
|
size=$(du -h "$OUTPUT" | cut -f1)
|
||||||
|
success "FAT seed image created: ${OUTPUT} (${size})"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Utilities
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
bool_str() {
|
||||||
|
if [[ "$1" == true ]]; then echo "yes"; else echo "no"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Summary
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
print_summary() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}Seed archive summary${RESET}"
|
||||||
|
echo -e " Application: ${CYAN}${APP}${RESET}"
|
||||||
|
echo -e " Defaults: $(if [[ "$APPLY_DEFAULTS" == true ]]; then echo "${GREEN}yes${RESET}"; else echo "${YELLOW}no${RESET}"; fi)"
|
||||||
|
echo -e " Certificate: $(if [[ -n "$CERT_CONTENT" ]]; then echo "${GREEN}injected${RESET}"; else echo "${DIM}none${RESET}"; fi)"
|
||||||
|
echo -e " Format: ${FORMAT}"
|
||||||
|
echo -e " Output: ${BOLD}${OUTPUT}${RESET}"
|
||||||
|
|
||||||
|
if [[ -n "$DISK_TARGET" ]]; then
|
||||||
|
echo -e " Disk target: ${DISK_TARGET}"
|
||||||
|
fi
|
||||||
|
if [[ "$MISSING_TPM" == true ]]; then
|
||||||
|
echo -e " Security: ${YELLOW}TPM not required${RESET}"
|
||||||
|
fi
|
||||||
|
if [[ "$MISSING_SECUREBOOT" == true ]]; then
|
||||||
|
echo -e " Security: ${YELLOW}Secure Boot not required${RESET}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ "$FORMAT" == "tar" ]]; then
|
||||||
|
echo -e "${DIM} Use with flasher-tool:${RESET}"
|
||||||
|
echo -e " flasher-tool --seed-tar ${OUTPUT}"
|
||||||
|
elif [[ "$FORMAT" == "fat" ]]; then
|
||||||
|
echo -e "${DIM} Attach as secondary disk/ISO to the VM.${RESET}"
|
||||||
|
echo -e "${DIM} IncusOS will detect the SEED_DATA label at boot.${RESET}"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
main() {
|
||||||
|
setup_colors
|
||||||
|
parse_args "$@"
|
||||||
|
validate_args
|
||||||
|
|
||||||
|
if [[ "$QUIET" != true ]] && [[ "$DRY_RUN" != true ]]; then
|
||||||
|
echo -e "${BOLD}${SCRIPT_NAME}${RESET} v${VERSION}"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
echo -e "${BOLD}Dry run -- showing generated seed files${RESET}"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
setup_workdir
|
||||||
|
detect_client_cert
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Generate all seed files
|
||||||
|
generate_install_yaml
|
||||||
|
generate_applications_yaml
|
||||||
|
|
||||||
|
case "$APP" in
|
||||||
|
incus)
|
||||||
|
generate_incus_yaml
|
||||||
|
;;
|
||||||
|
operations-center)
|
||||||
|
generate_ops_center_yaml
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
generate_network_yaml
|
||||||
|
generate_update_yaml
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create output
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
info "Dry run complete -- no files were created"
|
||||||
|
else
|
||||||
|
case "$FORMAT" in
|
||||||
|
tar) create_tar_output ;;
|
||||||
|
fat) create_fat_output ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ "$QUIET" != true ]]; then
|
||||||
|
print_summary
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
# IncusOS ISO Download & Customization Methods
|
||||||
|
|
||||||
|
> **Note:** This document was the original research note that started this repository.
|
||||||
|
> For the actual automation scripts, see [`../incusos/README.md`](../incusos/README.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
To automate the generation and download of customized IncusOS ISO files, you have two technical paths: using the official **flasher-tool** (the recommended CLI method for automation) or scripting the **REST API** used by the web customizer.
|
||||||
|
|
||||||
|
### Method 1: The flasher-tool (Recommended for Scripts)
|
||||||
|
|
||||||
|
The Incus team provides a dedicated command-line utility specifically for automated image generation. It connects to the Linux Containers CDN to fetch the latest builds and applies your customizations locally.
|
||||||
|
|
||||||
|
**Installation:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install github.com/lxc/incus-os/incus-osd/cmd/flasher-tool@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sample Automation Script:**
|
||||||
|
|
||||||
|
This script generates a customized ISO for an x86_64 cluster node with your local client certificate already trusted.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# automated-incusos-build.sh
|
||||||
|
|
||||||
|
# 1. Fetch your local Incus client certificate
|
||||||
|
CLIENT_CERT=$(incus remote get-client-certificate)
|
||||||
|
|
||||||
|
# 2. Define your customized seed in JSON format
|
||||||
|
# This example preps a node to join a cluster (apply_defaults: false)
|
||||||
|
cat <<EOF > cluster-node-seed.json
|
||||||
|
{
|
||||||
|
"apply_defaults": false,
|
||||||
|
"preseed": {
|
||||||
|
"certificates": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 3. Run the flasher tool to build the ISO
|
||||||
|
# -f: format (iso or img)
|
||||||
|
# -s: path to your seed file
|
||||||
|
# -o: output filename
|
||||||
|
flasher-tool -f iso -s cluster-node-seed.json -o my-custom-node.iso
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 2: Scripting the Web Customizer API
|
||||||
|
|
||||||
|
If you prefer to use the web backend directly via curl, you must POST a configuration payload to the generator endpoint. The web customizer is an SPA that communicates with a backend to stream a compressed image.
|
||||||
|
|
||||||
|
**Technical Logic:**
|
||||||
|
|
||||||
|
The backend accepts a JSON object that mimics the "Seed" structures. One critical detail is that the web customizer streams **gzipped** data which the browser typically decompresses on the fly; when using curl, you must ensure you handle the output stream correctly.
|
||||||
|
|
||||||
|
**Sample curl Download Script:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
ARCH="x86_64" # x86_64 or aarch64
|
||||||
|
APP="incus" # incus, operations-center, or migration-manager
|
||||||
|
USAGE="install" # install (on disk) or live (run from media)
|
||||||
|
CERT=$(incus remote get-client-certificate | sed ':a;N;$!ba;s/\n/\\n/g')
|
||||||
|
|
||||||
|
# Generate the JSON payload for the web customizer
|
||||||
|
PAYLOAD=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"format": "iso",
|
||||||
|
"architecture": "$ARCH",
|
||||||
|
"usage": "$USAGE",
|
||||||
|
"applications": ["$APP"],
|
||||||
|
"incus": {
|
||||||
|
"apply_defaults": true,
|
||||||
|
"preseed": {
|
||||||
|
"certificates": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
# POST to the customizer backend (Note: The URL may vary by build/release)
|
||||||
|
curl -X POST https://incusos-customizer.linuxcontainers.org/api/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$PAYLOAD" \
|
||||||
|
--output incus-os-latest.iso
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Reference for Customization Settings
|
||||||
|
|
||||||
|
When scripting either tool, use these field values to generate the "flavors" you need for your lab:
|
||||||
|
|
||||||
|
| Selection | Value Options | Description |
|
||||||
|
|--------------------|----------------------------------------------|----------------------------------------------------|
|
||||||
|
| **Architecture** | x86_64, aarch64 | Target CPU type. |
|
||||||
|
| **Applications** | incus, operations-center, migration-manager | Can be an array for multiple apps. |
|
||||||
|
| **Defaults** | true or false | Set to false for cluster nodes to avoid conflicts. |
|
||||||
|
| **Security** | missing_tpm: true | Use if hardware/VM lacks a TPM 2.0 module. |
|
||||||
|
|
||||||
|
### Advanced Automation Tip: Using a "Pristine" Image
|
||||||
|
|
||||||
|
If you find downloading multiple 500MB+ files is too slow, you can download one **Pristine ISO** (unconfigured) from the image server and provide the unique configuration via a small external partition labeled **SEED_DATA**.
|
||||||
|
|
||||||
|
- Download the base image once.
|
||||||
|
- Create a tiny (10MB) FAT image.
|
||||||
|
- Label that tiny image `SEED_DATA` and place your unique `incus.yaml` and `network.yaml` inside it.
|
||||||
|
- Mount both the ISO and the SEED_DATA disk to your VM. IncusOS will automatically merge them at boot.
|
||||||
Loading…
Reference in New Issue