Builds a Proxmox VE image that MAAS can deploy to bare metal, plus first-boot
automation that configures the node and joins it to a Proxmox cluster with no
manual steps.
The image starts from the official Debian cloud image and installs proxmox-ve
on top of it, rather than capturing a raw disk from the Proxmox ISO. That keeps
MAAS in control of partitioning, networking, SSH keys and cloud-init, and makes
moving between Proxmox releases a variable change instead of a rewrite.
Contents:
* Makefile driving the whole flow: build, verify, preseed, upload
* customize-proxmox.sh, run inside the Packer build VM, which layers Proxmox
onto the Debian cloud image and resets the pmxcfs node identity so one image
can produce many nodes
* pve-maas-init, a first-boot state machine covering /etc/hosts, node-unique
identifiers, the root password, vmbr0 conversion, cluster create/join and
the local-lvm thin pool; each stage is resumable across reboots
* curtin-hooks, which stops curtin installing a kernel over APT and pins
interface names by MAC so they match what MAAS recorded at commissioning
* a MAAS curtin preseed template and cloud-init examples
* deploy-cluster.sh, which builds a whole cluster through the MAAS API
* verify-image.sh, 22 static checks on the produced tarball
Cluster identity lives entirely in deploy-time cloud-init user-data, so a single
image and preseed can build any number of independent clusters.
Verified end to end against MAAS 3.7.2: proxmox-ve 9.2.0 / pve-manager 9.2.11 /
kernel 7.0.14-15-pve, deployed to two machines that formed a quorate cluster with
local-lvm on both, with no manual intervention.
The README documents four failure modes found along the way that all fail
silently: curtin rejecting "kernel: null", pvenetcommit overwriting the network
configuration at boot, interface renaming leaving the link down, and a systemd
ordering cycle that made systemd delete the service's start job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
115 lines
4.0 KiB
Python
Executable File
115 lines
4.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# curtin-hooks - MAAS/curtin kurulum kancasi (Proxmox VE imaji icin)
|
|
#
|
|
# Curtin, hedef sistemde /curtin/curtin-hooks varsa dahili curthooks yerine
|
|
# bu script'i calistirir (curtin.util.run_hook_if_exists).
|
|
#
|
|
# Amac: curtin'in APT uzerinden cekirdek kurmaya calismasini engellemek.
|
|
# Proxmox cekirdegi (proxmox-default-kernel) imajin icinde hazir gelir;
|
|
# curtin'in onu yeniden kurmasi gereksizdir ve dugumun deploy sirasinda
|
|
# download.proxmox.com'a erisebilmesini zorunlu kilar (izole aglarda calismaz).
|
|
#
|
|
# Not: Bunu preseed'de "kernel: null" ile yapmak mumkun degil - MAAS ile
|
|
# gelen curtin surumlerinin bir kismi bu degeri desteklemiyor ve
|
|
# install_kernel icinde AttributeError ile patliyor. Kancayi kullanmak
|
|
# curtin surumunden bagimsiz olarak calisir.
|
|
#
|
|
import shutil
|
|
import os
|
|
|
|
from curtin.commands import curthooks
|
|
from curtin.config import load_command_config
|
|
from curtin.util import load_command_environment
|
|
|
|
|
|
def disable_kernel_install():
|
|
"""curtin'in cekirdek kurulum adimini etkisiz hale getir.
|
|
|
|
Fonksiyon adi curtin surumune gore degisiyor (install_kernel ->
|
|
curthook_install_kernel), ikisini de yakaliyoruz.
|
|
"""
|
|
disabled = []
|
|
for name in ("install_kernel", "curthook_install_kernel"):
|
|
if hasattr(curthooks, name):
|
|
setattr(curthooks, name, lambda *args, **kwargs: None)
|
|
disabled.append(name)
|
|
print("curtin-hooks: cekirdek kurulumu devre disi: %s" % (disabled or "hicbiri bulunamadi"))
|
|
|
|
|
|
def pin_interface_names(config, target):
|
|
"""MAAS'in bildigi arayuz adlarini hedef sistemde MAC uzerinden sabitle.
|
|
|
|
MAAS commissioning'i Ubuntu ephemeral ortaminda yapiyor ve arayuzu orada
|
|
gordugu adla (or. enp6s18) kaydediyor. Deploy edilen Debian 13 / Proxmox
|
|
ise udev'in farkli isimlendirme semasi yuzunden ayni karti ens18 diye
|
|
adlandirabiliyor. Bu durumda cloud-init acilista arayuzu yeniden
|
|
adlandirmaya calisiyor, su hatayi aliyor:
|
|
|
|
Failed to rename devices: [busy] Error renaming
|
|
mac=... from ens18 to enp6s18
|
|
|
|
...ve arayuzu KAPALI birakiyor; dugum agini tamamen kaybediyor.
|
|
|
|
Cozum: MAC -> ad eslemesini systemd .link dosyasi olarak yaziyoruz, boylece
|
|
udev karti en bastan MAAS'in bekledigi adla olusturuyor ve yeniden
|
|
adlandirmaya hic gerek kalmiyor.
|
|
"""
|
|
net = config.get("network") or {}
|
|
pairs = []
|
|
|
|
# netplan / v2 bicimi
|
|
for name, cfg in (net.get("ethernets") or {}).items():
|
|
cfg = cfg or {}
|
|
mac = (cfg.get("match") or {}).get("macaddress")
|
|
if mac:
|
|
pairs.append((cfg.get("set-name") or name, mac))
|
|
|
|
# curtin v1 bicimi
|
|
for entry in net.get("config") or []:
|
|
if not isinstance(entry, dict) or entry.get("type") != "physical":
|
|
continue
|
|
if entry.get("name") and entry.get("mac_address"):
|
|
pairs.append((entry["name"], entry["mac_address"]))
|
|
|
|
if not pairs:
|
|
print("curtin-hooks: arayuz adi sabitlenmedi (ag yapilandirmasi bos)")
|
|
return
|
|
|
|
link_dir = os.path.join(target, "etc/systemd/network")
|
|
os.makedirs(link_dir, exist_ok=True)
|
|
for name, mac in pairs:
|
|
path = os.path.join(link_dir, "10-maas-%s.link" % name)
|
|
with open(path, "w") as fh:
|
|
fh.write(
|
|
"# MAAS tarafindan beklenen arayuz adi - pve-maas imaji\n"
|
|
"# curtin-hooks tarafindan olusturuldu.\n"
|
|
"[Match]\n"
|
|
"MACAddress=%s\n"
|
|
"\n"
|
|
"[Link]\n"
|
|
"Name=%s\n" % (mac.lower(), name)
|
|
)
|
|
print("curtin-hooks: arayuz adi sabitlendi %s -> %s" % (mac, name))
|
|
|
|
|
|
def cleanup():
|
|
"""Kancayi hedef sistemden kaldir - iz birakma."""
|
|
curtin_dir = os.path.dirname(os.path.abspath(__file__))
|
|
shutil.rmtree(curtin_dir, ignore_errors=True)
|
|
|
|
|
|
def main():
|
|
state = load_command_environment()
|
|
config = load_command_config(None, state)
|
|
target = state["target"]
|
|
|
|
disable_kernel_install()
|
|
curthooks.builtin_curthooks(config, target, state)
|
|
pin_interface_names(config, target)
|
|
cleanup()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|