Skip to content

Latest commit

 

History

12 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Cloud-init ISO Builder

A universal builder for generating NoCloud ISO images. Works as a regular ISO — attach it as a CD-ROM to a VM and cloud-init picks up the configuration automatically.

Why?

Proxmox's built-in cloud-init is limited: user, password, DNS, IP, SSH key, and package upgrade. Real-world provisioning needs more: custom packages, files, commands, multiple network interfaces, routing policies, bonds, VLANs.

This builder generates a standard ISO with cidata label that cloud-init auto-detects. Proxmox sees it as an ordinary CD-ROM.

Features

  • JSON configuration — explicit, unambiguous
  • Modular builds — split presets across main.json and conf.d/*.json
  • Multi-ID conflict resolution — compose setups by ID, override by priority
  • Include system — ship static assets (configs, binaries) alongside scripts
  • Multiple network interfaces — DHCP, static, bonds, bridges, VLANs
  • Binary-safe includes — base64-encoded for exact byte reproduction
  • Smart missing ID detection — checks both scripts and includes
  • Network v1/v2 — unified interfaces format; v1 (universal) or v2 (Netplan)
  • Postbuild — auto-deploy after ISO creation (global, per-build, or CLI)
  • Auto-clean output directory
  • CLI without config — inline builds with full param coverage
  • Config as JSON string — pass entire config via --config-json
  • Real .sh scripts inside ISO — readable, debuggable, version-controllable
  • Robust executionset -e + logging for every script

Quick Start

# Install dependencies
apt-get install -y python3-yaml genisoimage   # Debian/Ubuntu/Proxmox
dnf install -y python3-pyyaml genisoimage       # RHEL/Rocky/Alma
pacman -S python-yaml cdrtools                # Arch

# Build everything
python3 build.py -a

# Build a specific preset
python3 build.py --preset ubuntu-router

# Build multiple presets
python3 build.py --preset ubuntu-router,debian-web

# Build from a specific conf.d directory
python3 build.py -a --conf-d-build conf.d/production

# Inline build without any config file
python3 build.py --no-config \
  --profile ubuntu --ids default \
  --hostname myvm --ip 10.0.0.10/24 \
  --gateway 10.0.0.1

Project Structure

cibuilder/
├── build.py              # CLI frontend
├── lib/                  # Core library
│   ├── builder.py        # Build orchestration
│   ├── config.py         # Config loading + conf.d merging
│   ├── scripts.py        # Script resolution + includes
│   ├── network.py        # Network config generation
│   └── iso.py            # ISO creation + YAML writing
├── main.json             # Orchestrator: global + builds
├── conf.d/               # Modular build definitions
│   └── *.json
├── samples/              # Provisioning scripts + includes
│   ├── ubuntu/           # Profile: Ubuntu-based machines
│   │   ├── 01-base_default.sh
│   │   ├── 10-network_default.sh
│   │   └── include/
│   │       ├── default_motd
│   │       └── router_iptables.conf
│   ├── debian/           # Profile: Debian-based machines
│   ├── alpine/           # Profile: Alpine-based machines
│   ├── rhel/             # Profile: RHEL-based machines
│   └── common/           # Shared across all profiles
├── postbuild/
│   └── deploy-to-proxmox.sh
├── docs/
│   ├── usage.md          # User guide
│   └── tech.md           # Technical documentation
└── output/               # Generated ISOs and files

Configuration

main.json

{
  "global": {
    "output_dir": "./output",
    "iso_label": "cidata",
    "default_ssh_key": "~/.ssh/id_rsa.pub",
    "auto_clean": true,
    "postbuild": "postbuild/deploy-all.sh",
    "params": {
      "timezone": "UTC",
      "dns": "8.8.8.8",
      "domain": "local"
    }
  },
  "builds": [
    {
      "preset": "ubuntu-router",
      "profile": "ubuntu",
      "ids": ["default", "router"],
      "params": {
        "hostname": "router1",
        "ip": "dhcp",
        "interface": "ens18"
      }
    },
    {
      "preset": "ubuntu-web",
      "profile": "ubuntu",
      "ids": ["default"],
      "params": {
        "hostname": "web-01",
        "ciuser": "admin",
        "password": "changeme123",
        "ip": "10.0.0.10/24",
        "gateway": "10.0.0.1",
        "interface": "ens18"
      }
    }
  ]
}

conf.d/

// conf.d/routers.json
{
  "global": {
    "params": {
      "timezone": "Europe/Moscow"
    }
  },
  "builds": [
    {
      "preset": "router-east",
      "profile": "ubuntu",
      "ids": ["default", "router"],
      "params": {
        "hostname": "router-east",
        "ip": "dhcp"
      }
    }
  ]
}

Script Naming

{priority}-{name}_{id}.sh
  • 01-disk_default.sh → priority=1, name=disk, id=default
  • 10-network_router.sh → priority=10, name=network, id=router

When multiple scripts share a priority, the one whose id appears later in the build's ids array wins.

Include System

samples/{profile}/include/{id}_{name}
samples/ubuntu/include/
  default_motd              → /opt/provision/include/motd
  router_iptables.conf      → /opt/provision/include/iptables.conf

Include files are base64-encoded in user-data to ensure binary files are reproduced exactly.

CLI Usage

Config Modes

# Show help
python3 build.py

# Build all
python3 build.py -a

# Pass config as JSON string
python3 build.py --config-json '{"builds":[{"preset":"web","profile":"ubuntu","ids":["default"],"params":{"hostname":"web1"}}]}'

# Ignore existing main.json
python3 build.py --no-config --profile ubuntu --ids default --hostname test

# Build from specific conf.d
python3 build.py -a --conf-d-build conf.d/production

Build Selection

# Specific preset
python3 build.py --preset ubuntu-router

# Multiple presets
python3 build.py --preset ubuntu-router,debian-db

# Filter by profile
python3 build.py -a --profile ubuntu,debian

# Override IDs
python3 build.py -a --ids default,router

# Preview
python3 build.py -a --dry-run

# List presets
python3 build.py --list

Inline Build

# Minimal
python3 build.py --profile ubuntu --ids default \
  --hostname myvm --ip 10.0.0.10/24 --gateway 10.0.0.1

# Full inline
python3 build.py --no-config \
  --profile ubuntu --ids default \
  --hostname router --ip dhcp \
  --ciuser admin --password changeme \
  --packages nginx,iptables-persistent \
  --timezone Europe/Moscow \
  --growpart --package-update \
  --script ./custom-setup.sh

# Multiple interfaces
python3 build.py --profile ubuntu --ids default \
  --hostname router \
  --interfaces '[{"name":"ens18","type":"dhcp"},{"name":"ens19","type":"static","address":"192.168.50.10","netmask":"255.255.255.0"}]'

Output Control

# Files only
python3 build.py -a --no-iso

# Skip auto-clean
python3 build.py -a --no-clean

# Skip postbuild
python3 build.py -a --no-postbuild

# Custom postbuild
python3 build.py -a --postbuild postbuild/deploy-to-proxmox.sh

# Verbose
python3 build.py -a -v

# Quiet
python3 build.py -a -q

Connecting ISO to Proxmox

qm set <vmid> --ide2 local:iso/{preset-name}.iso,media=cdrom
qm set <vmid> --sata0 local:iso/{preset-name}.iso,media=cdrom

Via GUI: Hardware → Add → CD/DVD Drive → select ISO.

Documentation

Cross-Profile Compatibility

Profile Init Packages Network Support
Ubuntu/Debian systemd apt systemd-networkd/Netplan v1 + v2
Rocky/Alma/RHEL systemd dnf/yum NetworkManager v1
Alpine OpenRC apk ifupdown v1
Arch systemd pacman systemd-networkd v1 + v2

Dependencies

Debian / Ubuntu / Proxmox:

apt-get install -y python3-yaml genisoimage

RHEL / Rocky / AlmaLinux:

dnf install -y python3-pyyaml genisoimage

Arch Linux:

pacman -S python-yaml cdrtools

NixOS:

{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
  buildInputs = [ pkgs.python3 pkgs.python3Packages.pyyaml pkgs.cdrkit ];
}

License

MIT

About

Simple Cloud-init builder

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Contributors

Languages