Initial commit: MAAS-deployable Samba Active Directory domain controller

Builds a Debian image that MAAS deploys to bare metal as an Active Directory
domain controller, with first-boot automation that either creates a domain or
joins an existing one without anyone logging in. To a Windows client the result
is an AD domain: same Kerberos, same LDAP, same Group Policy, same domain join.

The request was for "primary and backup domain controllers", which is NT 4
terminology. Active Directory has no such split — every DC holds a writable copy
and any of them can service a logon. What lives on one DC at a time are the five
FSMO roles, one confusingly named "PDC Emulator". The image therefore offers
provision (create the domain) and join (add another equal DC), which is the
distinction that actually exists.

Contents:

  * customize-samba-ad.sh, run inside the packer-maas build VM: installs samba,
    winbind, Kerberos, chrony and rsync, swaps the cloud kernel for the generic
    one, and deletes every trace of a domain so one image can produce many DCs
  * adc-maas-init, a first-boot state machine covering /etc/hosts, node-unique
    identifiers, time, the resolver, provisioning or joining, the service
    switchover and a self-test that proves Kerberos issues a ticket
  * adc-sysvol-sync, a systemd timer implementing the SYSVOL workaround
  * a MAAS curtin preseed, cloud-init examples, and the release pipeline

Two findings shaped the design.

Debian builds Samba against its bundled Heimdal rather than system MIT Kerberos
— samba-ad-dc does not depend on krb5-kdc — so the AD DC role is in Samba's
supported configuration, not the experimental MIT one. The build asserts this
and fails if it ever changes.

Samba implements neither DFS-R nor FRS, so SYSVOL — where Group Policy lives —
does not replicate between DCs. Left alone, a policy created on one DC never
reaches the others and clients behave differently depending on which DC answered
them, with nothing reporting an error. adc-sysvol-sync applies the Samba wiki's
rsync workaround: sync idmap.ldb once so SID-to-uid mappings agree, rsync the
tree, then samba-tool ntacl sysvolreset because rsync carries POSIX bits while
the Windows ACLs live in AD. Without an SSH key to the source DC it exits with
an explanation rather than letting Group Policy diverge quietly.

Also carried over from maas-proxmox, where they were verified on real hardware:
the curtin-hooks that skip the kernel install and pin interface names by MAC,
Type=simple on the first-boot unit to avoid the systemd ordering cycle, and
shipping networking.service disabled so MAAS owns the network on first boot.
Specific to this image, the Debian cloud kernel is replaced with the generic one
— a bare-metal node booted with the cloud kernel can come up with no disk.

Nothing here has been built or deployed. The README says so at the top and in a
Verified status section that separates what was checked from what was not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-05 14:44:37 +02:00
commit cf07702535
25 changed files with 3158 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
#!/bin/bash
# Copyright (C) 2026 Ilker Manap
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Moves the built image into dist/ under a release-friendly name and writes the
# checksum plus the corresponding-source offer that has to accompany a binary
# distribution of GPL/AGPL software.
#
# assemble-artifacts.sh <samba-version>
set -euo pipefail
cd "$(dirname "$0")/../.."
VERSION="${1:?samba version required}"
OUT=$(make -s print-var VAR=OUTPUT)
ARCH=$(make -s print-var VAR=ARCH)
[ -f "$OUT" ] || { echo "built image not found: $OUT" >&2; exit 1; }
# Named after what it is - a MAAS image carrying a Samba AD domain
# controller. Nothing here is a Microsoft or Samba product.
NAME="maas-image-samba-ad-${VERSION}-${ARCH}.tar.gz"
rm -rf dist && mkdir -p dist
mv "$OUT" "dist/${NAME}"
( cd dist && sha256sum "$NAME" > "${NAME}.sha256" )
tar xzf "dist/${NAME}" -O ./etc/adc-maas/image-info > dist/image-info.txt 2>/dev/null \
|| echo "(no image-info found)" > dist/image-info.txt
DEB_VER=$(make -s print-var VAR=DEBIAN_VERSION)
DEB_SUITE=$(make -s print-var VAR=DEBIAN_SERIES)
REPO_URL="${GITHUB_SERVER_URL:-}/${GITHUB_REPOSITORY:-}"
{
echo "# Corresponding source"
echo
echo "This image is an unmodified installation of packages from the archives listed"
echo "below. No package was patched. What is ours is the selection and the"
echo "configuration, and that is the entire content of the repository linked below."
echo
echo "Written offer, per GPLv2 section 3 and GPLv3 section 6: the complete"
echo "corresponding source for every package in this image is available from the"
echo "archive it was installed from, at the versions recorded in the image's own"
echo "package database (\`/var/lib/dpkg/status\`)."
echo
echo "| Component | Source |"
echo "|---|---|"
echo "| Debian ${DEB_VER} \"${DEB_SUITE}\" | <https://deb.debian.org/debian> — \`apt-get source <pkg>\` |"
echo "| Build recipe | <${REPO_URL}> |"
echo
echo "Samba is GPLv3+; the Debian source package carries the full terms."
echo
echo "## Image metadata"
echo
echo '```'
cat dist/image-info.txt
echo '```'
} > dist/SOURCES.md
ls -lh dist/

95
scripts/ci/decide-build.sh Executable file
View File

@@ -0,0 +1,95 @@
#!/bin/bash
# Copyright (C) 2026 Ilker Manap
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Decides whether a rebuild is worth doing, and prints the decision as
# key=value lines suitable for $GITHUB_OUTPUT.
#
# decide-build.sh [max-age-days] (default 30)
#
# A rebuild happens when any of these is true:
#
# 1. samba in the Debian suite differs from the published image
# 2. the kernel package differs — kernel security fixes do not bump samba
# 3. the newest release is older than max-age-days — most Debian security
# updates touch neither, so without a floor an image could sit unchanged
# for months while its openssl and glibc went stale
#
# Requires GITEA_API and GITEA_TOKEN in the environment.
set -euo pipefail
cd "$(dirname "$0")/../.."
MAX_AGE_DAYS="${1:-30}"
: "${GITEA_API:?GITEA_API required}"
: "${GITEA_TOKEN:?GITEA_TOKEN required}"
SUITE=$(make -s print-var VAR=DEBIAN_SERIES)
ARCH=$(make -s print-var VAR=ARCH)
# Newest version of each package we care about, in one pass over the index.
PKGS=$(curl -fsS "http://deb.debian.org/debian/dists/${SUITE}/main/binary-${ARCH}/Packages.gz" | gunzip)
newest() {
printf '%s\n' "$PKGS" \
| awk -v want="$1" '$1=="Package:" && $2==want {p=1; next} p && $1=="Version:" {print $2; p=0}' \
| sort -V | tail -1
}
UP_SAMBA=$(newest samba)
UP_KERNEL=$(newest "linux-image-${ARCH}")
[ -n "$UP_SAMBA" ] || { echo "samba not found in Debian ${SUITE}" >&2; exit 1; }
[ -n "$UP_KERNEL" ] || { echo "linux-image-${ARCH} not found" >&2; exit 1; }
echo "upstream: samba=${UP_SAMBA} linux-image=${UP_KERNEL}" >&2
# What the newest published release actually contains. image-info.txt is a few
# hundred bytes and is published with every release, so this needs no guessing
# from tag names.
LATEST=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" "${GITEA_API}/releases?limit=1")
INFO_URL=$(printf '%s' "$LATEST" | python3 -c '
import json, sys
r = json.load(sys.stdin)
if r:
for a in r[0].get("assets", []):
if a["name"] == "image-info.txt":
print(a["browser_download_url"]); break
')
CREATED=$(printf '%s' "$LATEST" | python3 -c '
import json, sys
r = json.load(sys.stdin); print(r[0]["created_at"] if r else "")')
PREV_SAMBA="" PREV_KERNEL=""
if [ -n "$INFO_URL" ]; then
INFO=$(curl -fsSL "$INFO_URL" || true)
PREV_SAMBA=$(printf '%s' "$INFO" | awk -F= '$1=="samba"{print $2}')
PREV_KERNEL=$(printf '%s' "$INFO" | awk -F= '$1=="kernel"{print $2}')
fi
echo "published: samba=${PREV_SAMBA:-<none>} kernel=${PREV_KERNEL:-<none>}" >&2
AGE_DAYS=99999
if [ -n "$CREATED" ]; then
AGE_DAYS=$(CREATED="$CREATED" python3 -c '
import datetime, os
c = datetime.datetime.fromisoformat(os.environ["CREATED"].replace("Z", "+00:00"))
print((datetime.datetime.now(datetime.timezone.utc) - c).days)')
echo "newest release is ${AGE_DAYS} day(s) old" >&2
fi
BUILD=no
REASON="up to date"
if [ -z "$PREV_SAMBA" ]; then
BUILD=yes; REASON="no published image yet"
elif [ "$UP_SAMBA" != "$PREV_SAMBA" ]; then
BUILD=yes; REASON="samba ${PREV_SAMBA} -> ${UP_SAMBA}"
elif [ "$UP_KERNEL" != "$PREV_KERNEL" ]; then
BUILD=yes; REASON="kernel ${PREV_KERNEL} -> ${UP_KERNEL}"
elif [ "$AGE_DAYS" -ge "$MAX_AGE_DAYS" ]; then
BUILD=yes; REASON="image is ${AGE_DAYS} days old (limit ${MAX_AGE_DAYS}) — picking up Debian updates"
fi
echo "decision: ${BUILD} (${REASON})" >&2
printf 'build=%s\n' "$BUILD"
# Debian versions carry epochs and tildes; a tag cannot.
SAFE_VER=$(printf '%s' "$UP_SAMBA" | sed 's/^[0-9]*://; s/[^A-Za-z0-9._-]/-/g')
printf 'tag=%s\n' "samba-${SAFE_VER}-$(date -u +%Y%m%d)"
printf 'version=%s\n' "$SAFE_VER"
printf 'kernel=%s\n' "$UP_KERNEL"
printf 'reason=%s\n' "$REASON"

29
scripts/ci/prune-releases.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/bash
# Copyright (C) 2026 Ilker Manap
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Keeps the newest N releases and deletes the rest, tags included.
# Each image is ~1.5 GB, so unbounded retention fills the server.
#
# prune-releases.sh [keep] (default 3)
#
# Requires GITEA_API and GITEA_TOKEN in the environment.
set -euo pipefail
KEEP="${1:-3}"
: "${GITEA_API:?GITEA_API required}"
: "${GITEA_TOKEN:?GITEA_TOKEN required}"
curl -fsS -H "Authorization: token ${GITEA_TOKEN}" "${GITEA_API}/releases?limit=50" \
| KEEP="$KEEP" python3 -c '
import json, os, sys
keep = int(os.environ["KEEP"])
for r in json.load(sys.stdin)[keep:]:
print(r["id"], r["tag_name"])
' | while read -r id tag; do
echo "removing old release ${tag}"
curl -fsS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_API}/releases/${id}" || true
curl -fsS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_API}/tags/${tag}" || true
done

81
scripts/ci/publish-release.sh Executable file
View File

@@ -0,0 +1,81 @@
#!/bin/bash
# Copyright (C) 2026 Ilker Manap
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Creates a Gitea release and uploads the image, its checksum and the
# corresponding-source offer.
#
# publish-release.sh <tag> <version> <dist-dir>
#
# Requires GITEA_API and GITEA_TOKEN in the environment.
set -euo pipefail
TAG="${1:?tag required}"
VERSION="${2:?version required}"
DIST="${3:?dist directory required}"
# DIST cagiranin dizinine goreli olabilir; repo kokune gecmeden once mutlaklastir.
DIST="$(cd "$DIST" && pwd)"
# make degiskenlerini okuyabilmek icin depo koku gerekiyor.
cd "$(dirname "$0")/../.."
: "${GITEA_API:?GITEA_API required}"
: "${GITEA_TOKEN:?GITEA_TOKEN required}"
REPO_URL="${GITHUB_SERVER_URL:-}/${GITHUB_REPOSITORY:-}"
RUN="${GITHUB_RUN_NUMBER:-manual}"
SHA="${GITHUB_SHA:-}"
IMAGE=$(basename "$(ls "$DIST"/maas-image-*.tar.gz)")
MAAS_ARCH=$(make -s print-var VAR=MAAS_ARCH)
IMAGE_NAME=$(make -s print-var VAR=IMAGE_NAME)
BODY=$(cat <<BODYEOF
MAAS-deployable image of a Samba Active Directory domain controller.
- samba: \`${VERSION}\`
- Built: $(date -u +%Y-%m-%dT%H:%M:%SZ), run ${RUN}
- Verify: \`sha256sum -c ${IMAGE}.sha256\`
Upload to MAAS:
\`\`\`bash
maas \$PROFILE boot-resources create name='custom/${IMAGE_NAME}' \\
title='Samba AD DC' architecture='${MAAS_ARCH}' \\
filetype='tgz' content@=${IMAGE}
\`\`\`
The curtin preseed from this repository must also be installed on the MAAS region
controller, otherwise deployment fails — see the README.
\`SOURCES.md\` in this release carries the corresponding-source offer required by
the GPL/AGPL licences of the packages inside the image.
BODYEOF
)
PAYLOAD=$(TAG="$TAG" VERSION="$VERSION" BODY="$BODY" SHA="$SHA" python3 -c '
import json, os
print(json.dumps({
"tag_name": os.environ["TAG"],
"name": "Samba AD DC image (samba %s)" % os.environ["VERSION"],
"body": os.environ["BODY"],
"draft": False,
"prerelease": False,
"target_commitish": os.environ["SHA"],
}))')
ID=$(curl -fsS -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H 'Content-Type: application/json' \
-d "$PAYLOAD" "${GITEA_API}/releases" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')
echo "release id: ${ID}"
for f in "$DIST"/*; do
[ -f "$f" ] || continue
echo "uploading $(basename "$f") ($(du -h "$f" | cut -f1))"
curl -fsS -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-F "attachment=@${f}" \
"${GITEA_API}/releases/${ID}/assets?name=$(basename "$f")" >/dev/null
done
echo "published: ${REPO_URL}/releases/tag/${TAG}"

17
scripts/ci/upstream-version.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/bin/bash
# Copyright (C) 2026 Ilker Manap
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Prints the newest samba version in the configured Debian suite.
set -euo pipefail
cd "$(dirname "$0")/../.."
SUITE=$(make -s print-var VAR=DEBIAN_SERIES)
ARCH=$(make -s print-var VAR=ARCH)
VER=$(curl -fsS "http://deb.debian.org/debian/dists/${SUITE}/main/binary-${ARCH}/Packages.gz" \
| gunzip \
| awk '/^Package: samba$/{p=1; next} p && /^Version: /{print $2; p=0}' \
| sort -V | tail -1)
[ -n "$VER" ] || { echo "samba not found in Debian ${SUITE}" >&2; exit 1; }
printf '%s\n' "$VER"

View File

@@ -0,0 +1,247 @@
#!/bin/bash
# Copyright (C) 2026 Ilker Manap
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# customize-samba-ad.sh - runs inside the packer-maas build VM.
#
# Installs everything a Samba Active Directory domain controller needs on top of
# the Debian cloud image, then removes every trace of domain state so the image
# is generic. The domain itself is created or joined on first boot by
# adc-maas-init, driven by the cloud-init user-data MAAS supplies.
#
# This file is a template; the Makefile fills in the @@...@@ placeholders and
# appends the overlay archive, base64 encoded, after the marker at the end.
#
# Packer runs this with expect_disconnect = true.
set -euo pipefail
DEBIAN_SUITE="@@DEBIAN_SERIES@@"
AD_EXTRA_PACKAGES="@@AD_EXTRA_PACKAGES@@"
PACKER_MAAS_REF="@@PM_REF@@"
export DEBIAN_FRONTEND=noninteractive
APT="apt-get -y -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef"
use_eatmydata() {
command -v eatmydata >/dev/null 2>&1 || return 0
APT="eatmydata ${APT}"
log "eatmydata enabled (dpkg fsync calls disabled)"
}
log() { echo "==> [ad-image] $*"; }
# ---------------------------------------------------------------------------
# 1. Unpack the overlay embedded at the end of this script
# ---------------------------------------------------------------------------
log "unpacking the overlay"
sed -n '/^__ADC_MAAS_OVERLAY__$/,$p' "$0" | tail -n +2 | base64 -d \
| tar xzf - -C / --no-same-owner --no-same-permissions
chown -R root:root /usr/local/sbin/adc-maas-init /usr/local/sbin/adc-sysvol-sync \
/etc/adc-maas /etc/systemd/system/adc-maas-init.service \
/etc/systemd/system/adc-sysvol-sync.service \
/etc/systemd/system/adc-sysvol-sync.timer /curtin
chmod 0755 /usr/local/sbin/adc-maas-init /usr/local/sbin/adc-sysvol-sync
chmod 0755 /curtin /curtin/curtin-hooks
chmod 0644 /etc/adc-maas/adc-maas.conf
mkdir -p /etc/adc-maas/conf.d /var/lib/adc-maas
# ---------------------------------------------------------------------------
# 2. The build hostname has to resolve while packages configure themselves
# ---------------------------------------------------------------------------
BUILD_HOST="$(hostname -s)"
BUILD_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)}' | head -1)"
: "${BUILD_IP:=127.0.1.1}"
log "build hostname=${BUILD_HOST} ip=${BUILD_IP}"
cp -a /etc/hosts /etc/hosts.ad-image-backup
sed -i "/[[:space:]]${BUILD_HOST}\([[:space:]]\|$\)/d" /etc/hosts
echo "${BUILD_IP} ${BUILD_HOST}.local ${BUILD_HOST}" >> /etc/hosts
hostname -f || echo "WARNING: hostname -f does not resolve" >&2
# ---------------------------------------------------------------------------
# 3. Keep the MAAS-compatible cloud-init packer-maas installed
# ---------------------------------------------------------------------------
log "holding the cloud-init package"
apt-mark hold cloud-init || true
apt-get update
$APT install eatmydata || true
use_eatmydata
log "full-upgrade"
$APT full-upgrade
# ---------------------------------------------------------------------------
# 4. Answer the questions the packages would otherwise ask
# ---------------------------------------------------------------------------
# samba-common would offer to take WINS settings from DHCP, which is wrong for
# a DC. krb5-config wants a realm; the real one is not known until first boot,
# and adc-maas-init replaces /etc/krb5.conf with Samba's own anyway.
debconf-set-selections <<EOF
samba-common samba-common/dhcp boolean false
samba-common samba-common/do_debconf boolean false
krb5-config krb5-config/default_realm string EXAMPLE.LAN
krb5-config krb5-config/add_servers_realm string EXAMPLE.LAN
krb5-config krb5-config/read_conf boolean true
EOF
# ---------------------------------------------------------------------------
# 5. Defer initramfs regeneration until the end
# ---------------------------------------------------------------------------
log "diverting update-initramfs for the duration of the install"
dpkg-divert --local --rename --add /usr/sbin/update-initramfs >/dev/null
ln -sf /bin/true /usr/sbin/update-initramfs
# ---------------------------------------------------------------------------
# 6. The domain controller itself
#
# What an Active Directory domain actually consists of, all of it served by the
# single samba daemon unless noted:
#
# LDAP the directory: users, groups, computers, policy links
# Kerberos the KDC that issues the tickets clients authenticate with
# DNS AD is unusable without it — clients find DCs through SRV records
# SMB the SYSVOL and NETLOGON shares, where Group Policy lives
# NTP chrony; Kerberos rejects a clock skew over five minutes
# ---------------------------------------------------------------------------
log "installing the Samba AD domain controller"
$APT install \
samba samba-ad-dc samba-ad-provision samba-dsdb-modules \
winbind libnss-winbind libpam-winbind \
smbclient ldb-tools \
krb5-user \
chrony \
rsync
if [ -n "${AD_EXTRA_PACKAGES}" ]; then
log "installing extra packages: ${AD_EXTRA_PACKAGES}"
# shellcheck disable=SC2086
$APT install ${AD_EXTRA_PACKAGES}
fi
log "installed versions:"
dpkg-query -W -f='${Package} ${Version}\n' samba samba-ad-dc winbind krb5-user chrony || true
# Confirm this Samba can actually be a DC. Debian builds Samba against its
# bundled Heimdal; a build against system MIT Kerberos would make the AD DC role
# experimental, and samba-ad-dc would then pull in krb5-kdc.
if dpkg-query -W -f='${Depends}' samba-ad-dc 2>/dev/null | grep -q 'krb5-kdc'; then
echo "ERROR: this samba-ad-dc depends on the MIT KDC; the AD DC role is" >&2
echo " experimental in that configuration and is not used here." >&2
exit 1
fi
log "samba-ad-dc uses the bundled Heimdal KDC (the supported configuration)"
# ---------------------------------------------------------------------------
# 6b. Swap the cloud kernel for the generic one
#
# The Debian cloud image ships linux-image-cloud-amd64, which is built for
# virtual machines and leaves out most physical-hardware drivers. This image is
# deployed to bare metal by MAAS, so it needs the generic kernel or the node may
# come up with no disk or no network. curtin does not install a kernel here
# (see /curtin/curtin-hooks), so whatever is in the image is what boots.
# ---------------------------------------------------------------------------
DEB_ARCH="$(dpkg --print-architecture)"
log "installing the generic kernel (linux-image-${DEB_ARCH})"
$APT install "linux-image-${DEB_ARCH}"
CLOUD_KERNELS="$(dpkg-query -W -f='${Package}\n' 'linux-image-*cloud*' 2>/dev/null | grep -E '^linux-image' || true)"
if [ -n "${CLOUD_KERNELS}" ]; then
log "removing the cloud-only kernel: ${CLOUD_KERNELS}"
# shellcheck disable=SC2086
$APT purge ${CLOUD_KERNELS} || true
fi
$APT autoremove --purge || true
log "restoring update-initramfs and running it once"
rm -f /usr/sbin/update-initramfs
dpkg-divert --local --rename --remove /usr/sbin/update-initramfs >/dev/null
update-initramfs -u -k all
update-grub
# ---------------------------------------------------------------------------
# 7. Remove every trace of a domain
#
# Installing the packages leaves a default smb.conf and can leave TDB state. If
# any of it shipped in the image, every machine deployed from it would start
# from the same half-configured directory, and `samba-tool domain provision`
# would refuse to run at all.
# ---------------------------------------------------------------------------
log "clearing domain state so the image is generic"
systemctl stop samba-ad-dc smbd nmbd winbind 2>/dev/null || true
rm -f /etc/samba/smb.conf
rm -rf /var/lib/samba/private/* /var/lib/samba/sysvol/*
find /var/lib/samba -maxdepth 1 -name '*.tdb' -delete 2>/dev/null || true
find /var/cache/samba -type f -delete 2>/dev/null || true
rm -f /etc/krb5.keytab
# The standalone file-server daemons conflict with the AD DC daemon. Mask them
# now; adc-maas-init unmasks and starts samba-ad-dc once a domain exists.
systemctl disable smbd nmbd winbind 2>/dev/null || true
systemctl mask smbd nmbd winbind 2>/dev/null || true
systemctl disable samba-ad-dc 2>/dev/null || true
# Node-unique, must not be shared between machines built from this image.
rm -f /etc/iscsi/initiatorname.iscsi
rm -f /root/.ssh/known_hosts /etc/ssh/ssh_known_hosts
# ---------------------------------------------------------------------------
# 8. Leave the network to MAAS on the first boot
#
# ifupdown/ifupdown2 writes the build VM's interface name into
# /etc/network/interfaces. On a deployed node MAAS configures the network
# through netplan and systemd-networkd; if ifupdown also starts, it takes the
# real interface down using that stale definition and the node loses its network
# before any of our automation runs.
# ---------------------------------------------------------------------------
log "disabling networking.service (MAAS owns the network on first boot)"
cat > /etc/network/interfaces <<'EOF'
# Left deliberately minimal. The network on a deployed node is configured by
# MAAS through netplan and systemd-networkd.
auto lo
iface lo inet loopback
source /etc/network/interfaces.d/*
EOF
rm -f /etc/network/interfaces.d/* /etc/network/interfaces.new 2>/dev/null || true
systemctl disable networking.service 2>/dev/null || true
# ---------------------------------------------------------------------------
# 9. First-boot service, persistent logs, image metadata
# ---------------------------------------------------------------------------
log "enabling adc-maas-init.service"
systemctl daemon-reload
systemctl enable adc-maas-init.service
# The first boot can reboot once, and a volatile journal loses everything that
# happened before it — including why a stage failed.
log "enabling a persistent journal"
mkdir -p /var/log/journal
systemd-tmpfiles --create --prefix /var/log/journal 2>/dev/null || true
log "writing /etc/adc-maas/image-info"
{
echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "debian_suite=${DEBIAN_SUITE}"
echo "packer_maas_ref=${PACKER_MAAS_REF}"
dpkg-query -W -f='${Package}=${Version}\n' samba samba-ad-dc winbind krb5-user 2>/dev/null
echo "kernel=$(ls -1 /boot/vmlinuz-* 2>/dev/null | sed 's|.*/vmlinuz-||' | head -1)"
} > /etc/adc-maas/image-info
cat /etc/adc-maas/image-info
# ---------------------------------------------------------------------------
# 10. Clean up
# ---------------------------------------------------------------------------
log "cleaning up"
mv /etc/hosts.ad-image-backup /etc/hosts
$APT clean
rm -rf /var/lib/apt/lists/*
find /var/log/journal -mindepth 1 -delete 2>/dev/null || true
rm -rf /var/log/*.gz /var/log/*.1
: > /var/log/wtmp || true
: > /var/log/btmp || true
cloud-init clean --logs || true
log "image ready: Samba AD DC on Debian ${DEBIAN_SUITE}"
exit 0
# Everything after this marker is the base64 overlay archive the Makefile appends.

65
scripts/install-deps.sh Executable file
View File

@@ -0,0 +1,65 @@
#!/bin/bash
# Copyright (C) 2026 Ilker Manap
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# install-deps.sh - Ubuntu 22.04+ build host'una packer-maas bagimliliklarini kurar.
#
# Kullanim: sudo ./scripts/install-deps.sh
#
set -euo pipefail
if [ "$(id -u)" -ne 0 ]; then
echo "Bu script root olarak calistirilmali: sudo $0" >&2
exit 1
fi
. /etc/os-release
if [ "${ID:-}" != "ubuntu" ] && [ "${ID_LIKE:-}" != "debian" ]; then
echo "UYARI: Bu script Ubuntu/Debian icin yazildi (bulunan: ${PRETTY_NAME:-bilinmiyor})." >&2
fi
export DEBIAN_FRONTEND=noninteractive
echo "==> Temel paketler kuruluyor"
apt-get update
apt-get install -y --no-install-recommends \
ca-certificates curl gpg git make parted pigz jq \
qemu-system-x86 qemu-utils ovmf cloud-image-utils \
libnbd-bin nbdkit fuse2fs cpu-checker
# arm64 hedefi icin ek paketler. Varsayilan olarak kurulmaz: x86_64 host'ta
# arm64 derlemek TCG emulasyonu demektir (KVM yok) ve cok yavastir. Bu yol
# hic denenmedi - README'deki "Verified status" bolumune bakin.
if [ "${WITH_ARM64:-0}" = "1" ]; then
echo "==> arm64 hedefi icin ek paketler (WITH_ARM64=1)"
apt-get install -y --no-install-recommends qemu-system-arm qemu-efi-aarch64
fi
echo "==> HashiCorp APT deposu ekleniyor (packer)"
install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://apt.releases.hashicorp.com/gpg \
| gpg --dearmor --yes -o /etc/apt/keyrings/hashicorp-archive-keyring.gpg
chmod 0644 /etc/apt/keyrings/hashicorp-archive-keyring.gpg
cat > /etc/apt/sources.list.d/hashicorp.list <<REPO
deb [signed-by=/etc/apt/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com ${UBUNTU_CODENAME:-${VERSION_CODENAME}} main
REPO
apt-get update
apt-get install -y packer
echo "==> KVM kontrolu"
if ! kvm-ok; then
echo "HATA: KVM kullanilamiyor. VM'de nested virtualization acik mi? (Proxmox: cpu=host)" >&2
exit 1
fi
# Build root olarak kosuyor ama kullaniciyi da kvm grubuna alalim.
TARGET_USER="${SUDO_USER:-}"
if [ -n "$TARGET_USER" ] && [ "$TARGET_USER" != "root" ]; then
adduser "$TARGET_USER" kvm >/dev/null 2>&1 || true
fi
echo
echo "==> Hazir. Surumler:"
packer version
qemu-system-x86_64 --version | head -1

106
scripts/verify-image.sh Executable file
View File

@@ -0,0 +1,106 @@
#!/bin/bash
# Copyright (C) 2026 Ilker Manap
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# verify-image.sh - checks that the produced MAAS tarball contains what it should.
#
# Kullanim: ./scripts/verify-image.sh build/samba-ad-dc.tar.gz
#
set -uo pipefail
IMG="${1:-}"
[ -n "$IMG" ] && [ -f "$IMG" ] || { echo "Usage: $0 <imaj.tar.gz>" >&2; exit 2; }
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
echo "==> Imaj: $IMG ($(du -h "$IMG" | cut -f1))"
echo "==> Icerik listesi cikariliyor..."
tar tzf "$IMG" > "$TMP/list" || { echo "HATA: arsiv okunamadi"; exit 1; }
echo " $(wc -l < "$TMP/list") giris"
pass=0; fail=0
have() { grep -qx "\./$1" "$TMP/list" || grep -q "^\./$1$" "$TMP/list"; }
present() { grep -q "^\./$1" "$TMP/list"; }
check() {
local desc="$1" cond="$2"
if eval "$cond"; then
printf ' [ OK ] %s\n' "$desc"; pass=$((pass+1))
else
printf ' [FAIL] %s\n' "$desc"; fail=$((fail+1))
fi
}
echo
echo "=== Files that must be present ==="
check "adc-maas-init" 'present "usr/local/sbin/adc-maas-init"'
check "adc-sysvol-sync" 'present "usr/local/sbin/adc-sysvol-sync"'
check "adc-maas.conf" 'present "etc/adc-maas/adc-maas.conf"'
check "image-info" 'present "etc/adc-maas/image-info"'
check "adc-maas-init.service" 'present "etc/systemd/system/adc-maas-init.service"'
check "unit enabled for multi-user.target" 'present "etc/systemd/system/multi-user.target.wants/adc-maas-init.service"'
check "adc-sysvol-sync.timer" 'present "etc/systemd/system/adc-sysvol-sync.timer"'
check "curtin-hooks" 'present "curtin/curtin-hooks"'
check "samba daemon" 'present "usr/sbin/samba"'
check "samba-tool" 'present "usr/bin/samba-tool"'
check "winbindd" 'present "usr/sbin/winbindd"'
check "smbclient" 'present "usr/bin/smbclient"'
check "chronyd (Kerberos needs the clock)" 'present "usr/sbin/chronyd"'
check "rsync (SYSVOL replication)" 'present "usr/bin/rsync"'
check "cloud-init" 'present "usr/bin/cloud-init"'
check "generic kernel (not the cloud one)" 'grep -qE "^\\./boot/vmlinuz-.*-(amd64|arm64)$" "$TMP/list"'
echo
echo "=== Files that must NOT be present ==="
check "no smb.conf (image carries no domain)" '! present "etc/samba/smb.conf"'
check "no directory database (sam.ldb)" '! present "var/lib/samba/private/sam.ldb"'
check "no secrets.tdb" '! present "var/lib/samba/private/secrets.tdb"'
check "no krb5.keytab" '! present "etc/krb5.keytab"'
check "no iSCSI initiator name" '! present "etc/iscsi/initiatorname.iscsi"'
check "no SSH host keys" '! grep -qE "^\\./etc/ssh/ssh_host_.*_key$" "$TMP/list"'
check "networking.service NOT enabled" '! present "etc/systemd/system/multi-user.target.wants/networking.service"'
check "no interfaces.new" '! present "etc/network/interfaces.new"'
check "no cloud-only kernel" '! grep -qE "^\\./boot/vmlinuz-.*-cloud-(amd64|arm64)$" "$TMP/list"'
echo
echo "=== Kernel ==="
if grep -qE '^\./boot/vmlinuz-.*-(amd64|arm64)$' "$TMP/list"; then
printf ' [ OK ] generic kernel: %s\n' "$(grep -oE 'vmlinuz-[^ ]*' "$TMP/list" | head -1)"
pass=$((pass+1))
else
printf ' [FAIL] no generic (non-cloud) kernel under /boot\n'; fail=$((fail+1))
fi
echo
echo "==> Ayiklanan dosya icerikleri"
tar xzf "$IMG" -C "$TMP" \
./usr/local/sbin/adc-maas-init \
./etc/adc-maas/adc-maas.conf ./etc/adc-maas/image-info 2>/dev/null
tar xzf "$IMG" -C "$TMP" ./etc/network/interfaces 2>/dev/null
if [ -f "$TMP/etc/network/interfaces" ]; then
echo "--- /etc/network/interfaces ---"
sed 's/^/ /' "$TMP/etc/network/interfaces"
if grep -qE '^\s*(auto|iface)\s+(?!lo)' "$TMP/etc/network/interfaces" 2>/dev/null \
|| grep -qE '^[[:space:]]*iface[[:space:]]+[^l ]' "$TMP/etc/network/interfaces"; then
printf ' [FAIL] interfaces dosyasinda build VM artigi arayuz var\n'; fail=$((fail+1))
else
printf ' [ OK ] interfaces yalnizca loopback iceriyor\n'; pass=$((pass+1))
fi
fi
if [ -f "$TMP/etc/adc-maas/image-info" ]; then
echo "--- image-info ---"
sed 's/^/ /' "$TMP/etc/adc-maas/image-info"
fi
if [ -x "$TMP/usr/local/sbin/adc-maas-init" ]; then
printf ' [ OK ] adc-maas-init is executable\n'; pass=$((pass+1))
else
printf ' [FAIL] adc-maas-init is not executable\n'; fail=$((fail+1))
fi
echo
echo "==> Result: ${pass} passed, ${fail} failed"
[ "$fail" -eq 0 ]