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:
59
scripts/ci/assemble-artifacts.sh
Executable file
59
scripts/ci/assemble-artifacts.sh
Executable 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
95
scripts/ci/decide-build.sh
Executable 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
29
scripts/ci/prune-releases.sh
Executable 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
81
scripts/ci/publish-release.sh
Executable 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
17
scripts/ci/upstream-version.sh
Executable 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"
|
||||
Reference in New Issue
Block a user