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>
637 lines
24 KiB
Bash
Executable File
637 lines
24 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# pve-maas-init - MAAS ile deploy edilen Proxmox VE dugumunu ilk acilista yapilandirir.
|
|
#
|
|
# Asamalar (her biri /var/lib/pve-maas/<asama>.done ile bir kez calisir):
|
|
# hosts - hostname/FQDN'i yonetim IP'sine bagla (pvecm bunu ister)
|
|
# identity - dugume ozel kimlikleri yeniden uret (iSCSI IQN vb.)
|
|
# rootpw - root@pam parolasini ayarla (web arayuzu ve kume katilimi icin)
|
|
# network - MAAS'in verdigi arayuzu vmbr0 koprusune donustur
|
|
# cluster - kume olustur veya mevcut kumeye katil
|
|
# storage - bos alanda LVM-thin havuzu olustur (local-lvm)
|
|
#
|
|
# Yapilandirma: /etc/pve-maas/pve-maas.conf ve /etc/pve-maas/conf.d/*.conf
|
|
# (conf.d icerigi MAAS deploy sirasinda cloud-init write_files ile yazilir)
|
|
#
|
|
set -uo pipefail
|
|
|
|
CONF_DIR=/etc/pve-maas
|
|
STATE_DIR=/var/lib/pve-maas
|
|
LOG_TAG=pve-maas-init
|
|
|
|
mkdir -p "$STATE_DIR" "$CONF_DIR/conf.d"
|
|
|
|
log() { echo "[$LOG_TAG] $*"; logger -t "$LOG_TAG" -- "$*" 2>/dev/null || true; }
|
|
warn() { echo "[$LOG_TAG] UYARI: $*" >&2; logger -t "$LOG_TAG" -p user.warning -- "UYARI: $*" 2>/dev/null || true; }
|
|
die() { echo "[$LOG_TAG] HATA: $*" >&2; logger -t "$LOG_TAG" -p user.err -- "HATA: $*" 2>/dev/null || true; exit 1; }
|
|
|
|
done_flag() { echo "$STATE_DIR/$1.done"; }
|
|
is_done() { [ -e "$(done_flag "$1")" ]; }
|
|
mark_done() { date -Is > "$(done_flag "$1")"; }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Yapilandirmayi yukle
|
|
# ---------------------------------------------------------------------------
|
|
load_config() {
|
|
# shellcheck disable=SC1091
|
|
[ -r "$CONF_DIR/pve-maas.conf" ] && . "$CONF_DIR/pve-maas.conf"
|
|
local f
|
|
for f in "$CONF_DIR"/conf.d/*.conf; do
|
|
[ -r "$f" ] || continue
|
|
log "yapilandirma yukleniyor: $f"
|
|
# shellcheck disable=SC1090
|
|
. "$f"
|
|
done
|
|
|
|
# Varsayilanlar
|
|
PVE_ENABLED="${PVE_ENABLED:-true}"
|
|
PVE_FQDN="${PVE_FQDN:-}"
|
|
|
|
PVE_NET_MANAGE="${PVE_NET_MANAGE:-true}"
|
|
PVE_NET_BRIDGE="${PVE_NET_BRIDGE:-vmbr0}"
|
|
PVE_NET_UPLINK="${PVE_NET_UPLINK:-}"
|
|
PVE_NET_MODE="${PVE_NET_MODE:-auto}" # auto | static | dhcp
|
|
PVE_NET_APPLY="${PVE_NET_APPLY:-reboot}" # reboot | reload | none
|
|
PVE_NET_VLAN_AWARE="${PVE_NET_VLAN_AWARE:-false}"
|
|
PVE_NET_EXTRA="${PVE_NET_EXTRA:-}"
|
|
|
|
PVE_ROOT_PASSWORD="${PVE_ROOT_PASSWORD:-}"
|
|
PVE_ROOT_PASSWORD_HASH="${PVE_ROOT_PASSWORD_HASH:-}"
|
|
|
|
PVE_CLUSTER_MODE="${PVE_CLUSTER_MODE:-none}" # none | create | join
|
|
PVE_CLUSTER_NAME="${PVE_CLUSTER_NAME:-}"
|
|
PVE_CLUSTER_PEER="${PVE_CLUSTER_PEER:-}"
|
|
PVE_CLUSTER_PEER_PASSWORD="${PVE_CLUSTER_PEER_PASSWORD:-}"
|
|
PVE_CLUSTER_PEER_PASSWORD_FILE="${PVE_CLUSTER_PEER_PASSWORD_FILE:-}"
|
|
PVE_CLUSTER_FINGERPRINT="${PVE_CLUSTER_FINGERPRINT:-}"
|
|
PVE_CLUSTER_FINGERPRINT_DISCOVER="${PVE_CLUSTER_FINGERPRINT_DISCOVER:-true}"
|
|
PVE_CLUSTER_LINK0="${PVE_CLUSTER_LINK0:-}"
|
|
PVE_CLUSTER_LINK1="${PVE_CLUSTER_LINK1:-}"
|
|
PVE_CLUSTER_NODEID="${PVE_CLUSTER_NODEID:-}"
|
|
PVE_CLUSTER_VOTES="${PVE_CLUSTER_VOTES:-}"
|
|
PVE_CLUSTER_WAIT="${PVE_CLUSTER_WAIT:-900}"
|
|
PVE_CLUSTER_RETRIES="${PVE_CLUSTER_RETRIES:-5}"
|
|
PVE_CLUSTER_WIPE_SECRETS="${PVE_CLUSTER_WIPE_SECRETS:-true}"
|
|
|
|
PVE_THINPOOL="${PVE_THINPOOL:-auto}" # auto | off | <vg-adi>
|
|
PVE_THINPOOL_NAME="${PVE_THINPOOL_NAME:-data}"
|
|
PVE_THINPOOL_STORAGE="${PVE_THINPOOL_STORAGE:-local-lvm}"
|
|
PVE_THINPOOL_MIN_GB="${PVE_THINPOOL_MIN_GB:-16}"
|
|
PVE_THINPOOL_DISK="${PVE_THINPOOL_DISK:-}"
|
|
PVE_THINPOOL_VG="${PVE_THINPOOL_VG:-pve}"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Yardimcilar
|
|
# ---------------------------------------------------------------------------
|
|
primary_iface() {
|
|
if [ -n "$PVE_NET_UPLINK" ]; then echo "$PVE_NET_UPLINK"; return; fi
|
|
ip -4 -o route show default 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") {print $(i+1); exit}}'
|
|
}
|
|
|
|
primary_ip() {
|
|
ip -4 -o route get 1.1.1.1 2>/dev/null \
|
|
| awk '{for(i=1;i<=NF;i++) if($i=="src") {print $(i+1); exit}}'
|
|
}
|
|
|
|
node_fqdn() {
|
|
local host fqdn dom
|
|
host="$(hostname -s)"
|
|
if [ -n "$PVE_FQDN" ]; then echo "$PVE_FQDN"; return; fi
|
|
fqdn="$(hostname -f 2>/dev/null || true)"
|
|
if [ -n "$fqdn" ] && [ "$fqdn" != "$host" ]; then echo "$fqdn"; return; fi
|
|
dom="$(awk '/^(search|domain)[[:space:]]/{print $2; exit}' /etc/resolv.conf 2>/dev/null || true)"
|
|
if [ -n "$dom" ]; then echo "${host}.${dom}"; else echo "$host"; fi
|
|
}
|
|
|
|
wait_for_cloud_init() {
|
|
command -v cloud-init >/dev/null 2>&1 || return 0
|
|
log "cloud-init'in bitmesi bekleniyor"
|
|
# Unit'te After=cloud-final.service kullanamiyoruz (siralama dongusu),
|
|
# bu yuzden beklemeyi burada yapiyoruz. Hata donmesi normal olabilir.
|
|
timeout 900 cloud-init status --wait >/dev/null 2>&1
|
|
log "cloud-init durumu: $(cloud-init status 2>/dev/null | head -1)"
|
|
return 0
|
|
}
|
|
|
|
wait_for_network() {
|
|
local i ip
|
|
for ((i = 0; i < 120; i += 5)); do
|
|
ip="$(primary_ip)"
|
|
[ -n "$ip" ] && { log "yonetim IP'si: ${ip}"; return 0; }
|
|
sleep 5
|
|
done
|
|
return 1
|
|
}
|
|
|
|
wait_for_pmxcfs() {
|
|
local i
|
|
for i in $(seq 1 60); do
|
|
[ -e /etc/pve/local ] && return 0
|
|
sleep 2
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Asama: hosts
|
|
# ---------------------------------------------------------------------------
|
|
stage_hosts() {
|
|
is_done hosts && return 0
|
|
local host fqdn ip
|
|
host="$(hostname -s)"
|
|
fqdn="$(node_fqdn)"
|
|
ip="$(primary_ip)"
|
|
[ -n "$ip" ] || { warn "yonetim IP'si bulunamadi, /etc/hosts atlaniyor"; return 0; }
|
|
|
|
log "/etc/hosts guncelleniyor: ${ip} ${fqdn} ${host}"
|
|
cp -a /etc/hosts "$STATE_DIR/hosts.orig" 2>/dev/null || true
|
|
|
|
local keep
|
|
keep="$(grep -vE "^[[:space:]]*(127\.0\.0\.1|127\.0\.1\.1|::1|ff02::[12])[[:space:]]" /etc/hosts 2>/dev/null \
|
|
| grep -vE "[[:space:]]${host}([[:space:]]|\$)" || true)"
|
|
|
|
{
|
|
echo "127.0.0.1 localhost.localdomain localhost"
|
|
echo "${ip} ${fqdn} ${host}"
|
|
[ -n "$keep" ] && echo "$keep"
|
|
echo
|
|
echo "# IPv6"
|
|
echo "::1 localhost ip6-localhost ip6-loopback"
|
|
echo "ff02::1 ip6-allnodes"
|
|
echo "ff02::2 ip6-allrouters"
|
|
} > /etc/hosts
|
|
|
|
if command -v postconf >/dev/null 2>&1 && [ -f /etc/postfix/main.cf ]; then
|
|
postconf -e "myhostname = ${fqdn}" >/dev/null 2>&1 || true
|
|
systemctl try-restart postfix >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
# cloud-init manage_etc_hosts=true ise her acilista /etc/hosts'u
|
|
# 127.0.1.1 satiriyla geri yazar ve duzeltmemizi bozar.
|
|
cat > /etc/cloud/cloud.cfg.d/99-pve-maas-hosts.cfg <<'EOF'
|
|
# /etc/hosts artik pve-maas-init tarafindan yonetiliyor.
|
|
manage_etc_hosts: false
|
|
EOF
|
|
|
|
hostname -f >/dev/null 2>&1 || warn "hostname -f hala cozulmuyor"
|
|
|
|
# pmxcfs dugum kimligini ve sertifikalari dogru hostname/IP ile yenile.
|
|
if systemctl list-unit-files pve-cluster.service >/dev/null 2>&1; then
|
|
log "pve servisleri yeniden baslatiliyor (dogru hostname ile)"
|
|
systemctl restart pve-cluster >/dev/null 2>&1 || true
|
|
systemctl restart pvedaemon pveproxy pvestatd >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
mark_done hosts
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Asama: identity - dugume ozel kimlikler
|
|
# ---------------------------------------------------------------------------
|
|
stage_identity() {
|
|
is_done identity && return 0
|
|
if [ ! -s /etc/iscsi/initiatorname.iscsi ] && command -v iscsi-iname >/dev/null 2>&1; then
|
|
log "iSCSI initiator adi uretiliyor"
|
|
mkdir -p /etc/iscsi
|
|
echo "InitiatorName=$(iscsi-iname)" > /etc/iscsi/initiatorname.iscsi
|
|
chmod 0600 /etc/iscsi/initiatorname.iscsi
|
|
systemctl try-restart iscsid open-iscsi >/dev/null 2>&1 || true
|
|
fi
|
|
mark_done identity
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Asama: rootpw
|
|
# ---------------------------------------------------------------------------
|
|
stage_rootpw() {
|
|
is_done rootpw && return 0
|
|
if [ -n "$PVE_ROOT_PASSWORD_HASH" ]; then
|
|
log "root parolasi (hash) ayarlaniyor"
|
|
usermod -p "$PVE_ROOT_PASSWORD_HASH" root
|
|
elif [ -n "$PVE_ROOT_PASSWORD" ]; then
|
|
log "root parolasi ayarlaniyor"
|
|
echo "root:${PVE_ROOT_PASSWORD}" | chpasswd
|
|
else
|
|
warn "PVE_ROOT_PASSWORD tanimli degil; root@pam ile web arayuzune girilemez"
|
|
warn "ve bu dugum baska bir dugumun kumeye katilmasi icin hedef olamaz."
|
|
mark_done rootpw
|
|
return 0
|
|
fi
|
|
mark_done rootpw
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Asama: network - MAAS arayuzunu vmbr0'a cevir
|
|
# ---------------------------------------------------------------------------
|
|
detect_net_mode() {
|
|
[ "$PVE_NET_MODE" != "auto" ] && { echo "$PVE_NET_MODE"; return; }
|
|
if grep -rqsE '^\s*(dhcp4|dhcp6)\s*:\s*(true|yes)' /etc/netplan/ 2>/dev/null; then
|
|
echo dhcp; return
|
|
fi
|
|
if grep -rqsE '^\s*iface\s+\S+\s+inet6?\s+dhcp' /etc/network/interfaces.d/ 2>/dev/null; then
|
|
echo dhcp; return
|
|
fi
|
|
echo static
|
|
}
|
|
|
|
neutralize_foreign_netconf() {
|
|
log "MAAS/cloud-init ag yapilandirmasi devre disi birakiliyor"
|
|
printf 'network: {config: disabled}\n' > /etc/cloud/cloud.cfg.d/99-pve-maas-disable-network.cfg
|
|
|
|
local d
|
|
d=/etc/network/interfaces.d/disabled-by-pve-maas
|
|
mkdir -p "$d"
|
|
find /etc/network/interfaces.d -maxdepth 1 -type f -print0 2>/dev/null \
|
|
| xargs -0 -r -I{} mv {} "$d/"
|
|
|
|
if compgen -G "/etc/netplan/*.yaml" >/dev/null || compgen -G "/etc/netplan/*.yml" >/dev/null; then
|
|
mkdir -p /etc/netplan/disabled-by-pve-maas
|
|
mv /etc/netplan/*.y*ml /etc/netplan/disabled-by-pve-maas/ 2>/dev/null || true
|
|
fi
|
|
|
|
if systemctl is-enabled systemd-networkd >/dev/null 2>&1; then
|
|
systemctl disable --now systemd-networkd systemd-networkd.socket >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
# Proxmox statik /etc/resolv.conf bekler.
|
|
if [ -L /etc/resolv.conf ]; then
|
|
local ns search
|
|
ns="$(resolvectl dns 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9a-fA-F:.]+$' | sort -u || true)"
|
|
[ -z "$ns" ] && ns="$(awk '/^nameserver/{print $2}' /etc/resolv.conf | sort -u || true)"
|
|
search="$(awk '/^(search|domain)[[:space:]]/{$1=""; print substr($0,2); exit}' /etc/resolv.conf || true)"
|
|
if [ -n "$ns" ]; then
|
|
rm -f /etc/resolv.conf
|
|
{
|
|
[ -n "$search" ] && echo "search ${search}"
|
|
echo "$ns" | while read -r s; do [ -n "$s" ] && echo "nameserver $s"; done
|
|
} > /etc/resolv.conf
|
|
systemctl disable --now systemd-resolved >/dev/null 2>&1 || true
|
|
else
|
|
warn "nameserver bulunamadi, /etc/resolv.conf oldugu gibi birakiliyor"
|
|
fi
|
|
fi
|
|
|
|
systemctl enable networking.service >/dev/null 2>&1 || true
|
|
}
|
|
|
|
stage_network() {
|
|
is_done network && return 0
|
|
[ "$PVE_NET_MANAGE" = "true" ] || { log "ag yonetimi kapali (PVE_NET_MANAGE=false)"; mark_done network; return 0; }
|
|
|
|
local br uplink
|
|
br="$PVE_NET_BRIDGE"
|
|
uplink="$(primary_iface)"
|
|
|
|
if ip link show "$br" >/dev/null 2>&1 && [ "$uplink" = "$br" ]; then
|
|
log "$br zaten mevcut ve varsayilan rotayi tasiyor, ag donusumu atlaniyor"
|
|
mark_done network
|
|
return 0
|
|
fi
|
|
[ -n "$uplink" ] || { warn "uplink arayuzu bulunamadi, ag donusumu atlaniyor"; mark_done network; return 0; }
|
|
|
|
local mode cidr gw cidr6 gw6
|
|
mode="$(detect_net_mode)"
|
|
cidr="$(ip -4 -o addr show dev "$uplink" scope global 2>/dev/null | awk '{print $4; exit}')"
|
|
gw="$(ip -4 -o route show default dev "$uplink" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="via") {print $(i+1); exit}}')"
|
|
cidr6="$(ip -6 -o addr show dev "$uplink" scope global -deprecated 2>/dev/null | awk '{print $4; exit}')"
|
|
gw6="$(ip -6 -o route show default dev "$uplink" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="via") {print $(i+1); exit}}')"
|
|
|
|
if [ "$mode" = "static" ] && [ -z "$cidr" ]; then
|
|
warn "$uplink uzerinde IPv4 adresi yok, DHCP'ye dusuluyor"
|
|
mode=dhcp
|
|
fi
|
|
|
|
log "ag donusumu: ${uplink} -> ${br} (mod=${mode} adres=${cidr:-dhcp} gw=${gw:-yok})"
|
|
cp -a /etc/network/interfaces "$STATE_DIR/interfaces.orig" 2>/dev/null || true
|
|
neutralize_foreign_netconf
|
|
|
|
{
|
|
echo "# /etc/network/interfaces - pve-maas-init tarafindan olusturuldu"
|
|
echo "# Kaynak yedegi: $STATE_DIR/interfaces.orig"
|
|
echo
|
|
echo "auto lo"
|
|
echo "iface lo inet loopback"
|
|
echo
|
|
echo "iface ${uplink} inet manual"
|
|
echo
|
|
echo "auto ${br}"
|
|
if [ "$mode" = "dhcp" ]; then
|
|
echo "iface ${br} inet dhcp"
|
|
else
|
|
echo "iface ${br} inet static"
|
|
echo " address ${cidr}"
|
|
[ -n "$gw" ] && echo " gateway ${gw}"
|
|
fi
|
|
echo " bridge-ports ${uplink}"
|
|
echo " bridge-stp off"
|
|
echo " bridge-fd 0"
|
|
[ "$PVE_NET_VLAN_AWARE" = "true" ] && {
|
|
echo " bridge-vlan-aware yes"
|
|
echo " bridge-vids 2-4094"
|
|
}
|
|
if [ -n "$cidr6" ]; then
|
|
echo
|
|
echo "iface ${br} inet6 static"
|
|
echo " address ${cidr6}"
|
|
[ -n "$gw6" ] && echo " gateway ${gw6}"
|
|
fi
|
|
if [ -n "$PVE_NET_EXTRA" ]; then
|
|
echo
|
|
echo "# PVE_NET_EXTRA"
|
|
printf '%s\n' "$PVE_NET_EXTRA"
|
|
fi
|
|
echo
|
|
echo "source /etc/network/interfaces.d/*"
|
|
} > /etc/network/interfaces
|
|
|
|
# Proxmox'un pvenetcommit.service'i acilista /etc/network/interfaces.new
|
|
# dosyasini interfaces uzerine tasiyor. Kalmis bir .new dosyasi yeni
|
|
# yazdigimiz yapilandirmayi yeniden baslatmada ezer.
|
|
rm -f /etc/network/interfaces.new
|
|
|
|
mark_done network
|
|
|
|
case "$PVE_NET_APPLY" in
|
|
reload)
|
|
log "ifreload -a ile uygulaniyor"
|
|
ip -4 addr flush dev "$uplink" 2>/dev/null || true
|
|
ip -6 addr flush dev "$uplink" scope global 2>/dev/null || true
|
|
if ! ifreload -a; then
|
|
warn "ifreload basarisiz, yedek rota ekleniyor"
|
|
[ -n "$gw" ] && ip route replace default via "$gw" dev "$br" 2>/dev/null || true
|
|
fi
|
|
;;
|
|
reboot)
|
|
log "ag yapilandirmasi icin yeniden baslatiliyor; kalan asamalar sonraki acilista surecek"
|
|
# Bu servis hala calisirken 'systemctl reboot' cagirmak systemd islem
|
|
# kuyrugunda "transaction is destructive" hatasina yol acabilir; bu
|
|
# yuzden reboot'u ayri bir gecici unit uzerinden tetikliyoruz.
|
|
systemd-run --no-block --unit=pve-maas-reboot --on-active=5 \
|
|
/bin/systemctl reboot \
|
|
|| shutdown -r +1 "pve-maas-init: ag yapilandirmasi uygulanacak"
|
|
exit 0
|
|
;;
|
|
none)
|
|
log "PVE_NET_APPLY=none: yapilandirma yazildi, uygulanmadi"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Asama: cluster
|
|
# ---------------------------------------------------------------------------
|
|
peer_fingerprint() {
|
|
local peer="$1"
|
|
openssl s_client -connect "${peer}:8006" -servername "${peer}" </dev/null 2>/dev/null \
|
|
| openssl x509 -noout -fingerprint -sha256 2>/dev/null \
|
|
| cut -d= -f2
|
|
}
|
|
|
|
wait_for_peer() {
|
|
local peer="$1" timeout="$2" i
|
|
log "kume dugumu bekleniyor: ${peer}:8006 (en fazla ${timeout}s)"
|
|
for ((i = 0; i < timeout; i += 5)); do
|
|
if timeout 4 bash -c "exec 3<>/dev/tcp/${peer}/8006" 2>/dev/null; then
|
|
log "${peer}:8006 erisilebilir"
|
|
return 0
|
|
fi
|
|
sleep 5
|
|
done
|
|
return 1
|
|
}
|
|
|
|
cluster_password() {
|
|
if [ -n "$PVE_CLUSTER_PEER_PASSWORD_FILE" ] && [ -r "$PVE_CLUSTER_PEER_PASSWORD_FILE" ]; then
|
|
head -1 "$PVE_CLUSTER_PEER_PASSWORD_FILE"
|
|
else
|
|
printf '%s' "$PVE_CLUSTER_PEER_PASSWORD"
|
|
fi
|
|
}
|
|
|
|
wipe_cluster_secrets() {
|
|
[ "$PVE_CLUSTER_WIPE_SECRETS" = "true" ] || return 0
|
|
local f
|
|
for f in "$CONF_DIR"/conf.d/*.conf; do
|
|
[ -r "$f" ] || continue
|
|
grep -q 'PVE_CLUSTER_PEER_PASSWORD\|PVE_ROOT_PASSWORD' "$f" || continue
|
|
log "kimlik bilgileri temizleniyor: $f"
|
|
sed -i -E "s/^([[:space:]]*(PVE_CLUSTER_PEER_PASSWORD|PVE_ROOT_PASSWORD)(_HASH)?=).*/\1'<silindi>'/" "$f"
|
|
done
|
|
if [ -n "$PVE_CLUSTER_PEER_PASSWORD_FILE" ] && [ -f "$PVE_CLUSTER_PEER_PASSWORD_FILE" ]; then
|
|
shred -u "$PVE_CLUSTER_PEER_PASSWORD_FILE" 2>/dev/null \
|
|
|| rm -f "$PVE_CLUSTER_PEER_PASSWORD_FILE"
|
|
fi
|
|
}
|
|
|
|
wait_for_quorum() {
|
|
local i
|
|
for ((i = 0; i < 180; i += 5)); do
|
|
if pvecm status 2>/dev/null | grep -qE '^Quorate:[[:space:]]+Yes'; then
|
|
return 0
|
|
fi
|
|
sleep 5
|
|
done
|
|
return 1
|
|
}
|
|
|
|
stage_cluster() {
|
|
is_done cluster && return 0
|
|
[ "$PVE_CLUSTER_MODE" = "none" ] && { log "kume islemi yok (PVE_CLUSTER_MODE=none)"; mark_done cluster; return 0; }
|
|
|
|
wait_for_pmxcfs || { warn "/etc/pve baglanmadi, kume asamasi erteleniyor"; return 1; }
|
|
|
|
if [ -f /etc/pve/corosync.conf ]; then
|
|
log "dugum zaten bir kumenin parcasi, kume asamasi atlaniyor"
|
|
mark_done cluster
|
|
return 0
|
|
fi
|
|
|
|
local -a opts=()
|
|
[ -n "$PVE_CLUSTER_LINK0" ] && opts+=(--link0 "$PVE_CLUSTER_LINK0")
|
|
[ -n "$PVE_CLUSTER_LINK1" ] && opts+=(--link1 "$PVE_CLUSTER_LINK1")
|
|
[ -n "$PVE_CLUSTER_NODEID" ] && opts+=(--nodeid "$PVE_CLUSTER_NODEID")
|
|
[ -n "$PVE_CLUSTER_VOTES" ] && opts+=(--votes "$PVE_CLUSTER_VOTES")
|
|
|
|
case "$PVE_CLUSTER_MODE" in
|
|
create)
|
|
[ -n "$PVE_CLUSTER_NAME" ] || { warn "PVE_CLUSTER_NAME bos, kume olusturulamiyor"; return 1; }
|
|
log "kume olusturuluyor: ${PVE_CLUSTER_NAME}"
|
|
if pvecm create "$PVE_CLUSTER_NAME" "${opts[@]}"; then
|
|
wait_for_quorum && log "kume olusturuldu ve quorate"
|
|
mark_done cluster
|
|
else
|
|
warn "pvecm create basarisiz"
|
|
return 1
|
|
fi
|
|
;;
|
|
join)
|
|
[ -n "$PVE_CLUSTER_PEER" ] || { warn "PVE_CLUSTER_PEER bos, katilim yapilamiyor"; return 1; }
|
|
local pw fp
|
|
pw="$(cluster_password)"
|
|
[ -n "$pw" ] || { warn "kume dugumunun root parolasi verilmedi (PVE_CLUSTER_PEER_PASSWORD)"; return 1; }
|
|
|
|
wait_for_peer "$PVE_CLUSTER_PEER" "$PVE_CLUSTER_WAIT" \
|
|
|| { warn "kume dugumu ${PVE_CLUSTER_PEER} zaman asimina ugradi"; return 1; }
|
|
|
|
fp="$PVE_CLUSTER_FINGERPRINT"
|
|
if [ -z "$fp" ] && [ "$PVE_CLUSTER_FINGERPRINT_DISCOVER" = "true" ]; then
|
|
fp="$(peer_fingerprint "$PVE_CLUSTER_PEER")"
|
|
warn "parmak izi otomatik alindi (TOFU, dogrulanmadi): ${fp}"
|
|
fi
|
|
[ -n "$fp" ] || { warn "kume dugumunun sertifika parmak izi alinamadi"; return 1; }
|
|
|
|
local i rc=1
|
|
for ((i = 1; i <= PVE_CLUSTER_RETRIES; i++)); do
|
|
log "kumeye katiliniyor (deneme ${i}/${PVE_CLUSTER_RETRIES}): ${PVE_CLUSTER_PEER}"
|
|
# pvecm add etkilesimli parola sorar; API ucu ile etkilesimsiz calisiyoruz.
|
|
if pvesh create /cluster/config/join \
|
|
--hostname "$PVE_CLUSTER_PEER" \
|
|
--password "$pw" \
|
|
--fingerprint "$fp" \
|
|
"${opts[@]}"; then
|
|
rc=0
|
|
break
|
|
fi
|
|
warn "katilim basarisiz, 30s sonra tekrar denenecek"
|
|
sleep 30
|
|
done
|
|
|
|
if [ "$rc" -ne 0 ]; then
|
|
warn "kumeye katilim ${PVE_CLUSTER_RETRIES} denemede basarisiz"
|
|
return 1
|
|
fi
|
|
|
|
systemctl restart pve-cluster pvedaemon pveproxy pvestatd >/dev/null 2>&1 || true
|
|
if wait_for_quorum; then
|
|
log "kumeye katilim tamamlandi ve quorate"
|
|
else
|
|
warn "katilim yapildi ama quorum saglanamadi, kume durumunu kontrol edin"
|
|
fi
|
|
mark_done cluster
|
|
wipe_cluster_secrets
|
|
;;
|
|
*)
|
|
warn "gecersiz PVE_CLUSTER_MODE=${PVE_CLUSTER_MODE}"
|
|
mark_done cluster
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Asama: storage - LVM-thin havuzu
|
|
# ---------------------------------------------------------------------------
|
|
pick_vg() {
|
|
if [ "$PVE_THINPOOL" != "auto" ] && [ "$PVE_THINPOOL" != "off" ]; then
|
|
echo "$PVE_THINPOOL"; return
|
|
fi
|
|
vgs --noheadings --nosuffix --units b -o vg_name,vg_free 2>/dev/null \
|
|
| awk '{gsub(/^ +/,""); print $2, $1}' | sort -rn | head -1 | awk '{print $2}'
|
|
}
|
|
|
|
stage_storage() {
|
|
is_done storage && return 0
|
|
[ "$PVE_THINPOOL" = "off" ] && { log "thin havuz kapali"; mark_done storage; return 0; }
|
|
|
|
wait_for_pmxcfs || { warn "/etc/pve baglanmadi, storage asamasi erteleniyor"; return 1; }
|
|
command -v lvs >/dev/null 2>&1 || { warn "lvm2 yok"; mark_done storage; return 0; }
|
|
|
|
local vg="$PVE_THINPOOL_VG"
|
|
|
|
if [ -n "$PVE_THINPOOL_DISK" ]; then
|
|
if [ ! -b "$PVE_THINPOOL_DISK" ]; then
|
|
warn "PVE_THINPOOL_DISK=${PVE_THINPOOL_DISK} blok aygiti degil"
|
|
mark_done storage; return 0
|
|
fi
|
|
if ! pvs "$PVE_THINPOOL_DISK" >/dev/null 2>&1; then
|
|
log "PV/VG olusturuluyor: ${PVE_THINPOOL_DISK} -> ${vg}"
|
|
pvcreate -ff -y "$PVE_THINPOOL_DISK" || { warn "pvcreate basarisiz"; mark_done storage; return 0; }
|
|
vgcreate "$vg" "$PVE_THINPOOL_DISK" || { warn "vgcreate basarisiz"; mark_done storage; return 0; }
|
|
fi
|
|
else
|
|
vg="$(pick_vg)"
|
|
fi
|
|
|
|
[ -n "$vg" ] || { log "uygun VG bulunamadi, thin havuz atlaniyor"; mark_done storage; return 0; }
|
|
|
|
local pool="$PVE_THINPOOL_NAME"
|
|
if lvs "${vg}/${pool}" >/dev/null 2>&1; then
|
|
log "${vg}/${pool} zaten var"
|
|
else
|
|
local free_b free_g
|
|
free_b="$(vgs --noheadings --nosuffix --units b -o vg_free "$vg" 2>/dev/null | tr -d ' ')"
|
|
free_g=$(( ${free_b:-0} / 1024 / 1024 / 1024 ))
|
|
if [ "$free_g" -lt "$PVE_THINPOOL_MIN_GB" ]; then
|
|
log "${vg} icinde yalnizca ${free_g}GiB bos alan var (gereken ${PVE_THINPOOL_MIN_GB}GiB), thin havuz atlaniyor"
|
|
mark_done storage
|
|
return 0
|
|
fi
|
|
log "thin havuz olusturuluyor: ${vg}/${pool} (~${free_g}GiB)"
|
|
if ! lvcreate --type thin-pool -l 95%FREE -n "$pool" "$vg"; then
|
|
warn "lvcreate basarisiz"
|
|
mark_done storage
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
local storage="$PVE_THINPOOL_STORAGE" node
|
|
node="$(hostname -s)"
|
|
if pvesm status --storage "$storage" >/dev/null 2>&1; then
|
|
log "storage '${storage}' zaten tanimli"
|
|
local nodes
|
|
nodes="$(awk -v s="$storage" '$1=="lvmthin:" && $2==s {f=1; next} /^[a-z]+:/{f=0} f && $1=="nodes"{print $2}' /etc/pve/storage.cfg 2>/dev/null)"
|
|
if [ -n "$nodes" ] && ! echo ",$nodes," | grep -q ",${node},"; then
|
|
log "'${storage}' dugum listesine ${node} ekleniyor"
|
|
pvesm set "$storage" --nodes "${nodes},${node}" || warn "pvesm set basarisiz"
|
|
fi
|
|
else
|
|
log "storage tanimlaniyor: ${storage} (lvmthin ${vg}/${pool})"
|
|
local -a sopts=(--vgname "$vg" --thinpool "$pool" --content images,rootdir)
|
|
[ -f /etc/pve/corosync.conf ] && sopts+=(--nodes "$node")
|
|
pvesm add lvmthin "$storage" "${sopts[@]}" || warn "pvesm add basarisiz"
|
|
fi
|
|
|
|
mark_done storage
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
main() {
|
|
load_config
|
|
|
|
if [ "$PVE_ENABLED" != "true" ]; then
|
|
log "PVE_ENABLED=false, hicbir sey yapilmiyor"
|
|
exit 0
|
|
fi
|
|
|
|
log "baslatiliyor (hostname=$(hostname -s))"
|
|
|
|
wait_for_cloud_init
|
|
load_config # cloud-init conf.d dosyalarini yeni yazmis olabilir
|
|
|
|
if ! wait_for_network; then
|
|
warn "yonetim IP'si bulunamadi; sonraki acilista tekrar denenecek"
|
|
exit 1
|
|
fi
|
|
|
|
local failed=0
|
|
stage_hosts || failed=1
|
|
stage_identity || failed=1
|
|
stage_rootpw || failed=1
|
|
stage_network || failed=1
|
|
stage_cluster || failed=1
|
|
stage_storage || failed=1
|
|
|
|
if [ "$failed" -eq 0 ] \
|
|
&& is_done hosts && is_done identity && is_done rootpw \
|
|
&& is_done network && is_done cluster && is_done storage; then
|
|
date -Is > "$STATE_DIR/complete"
|
|
log "tum asamalar tamamlandi"
|
|
else
|
|
warn "bazi asamalar tamamlanmadi; servis sonraki acilista tekrar denenecek"
|
|
warn "ayrintilar: journalctl -u pve-maas-init"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
main "$@"
|