feat(linux): add Ubuntu 24.04 template VM automation scripts
Deploy-LinuxBuild2404.ps1 (host-side):
- Downloads Ubuntu 24.04 cloud VMDK (cached in F:\CI\ISO\)
- Generates cloud-init seed ISO (nocloud + guestinfo backup datasource)
- user-data: user+SSH key only no packages/runcmd, all provisioning
delegated to Prepare/Setup (removed -MinimalConfig, now always minimal)
- Assembles VMX, powers on VM, waits for cloud-init completion via SSH
- Removes seed ISO from VMX after first boot (ide1:0 disabled)
- Next-steps message uses correct -VMIPAddress parameter
Prepare-LinuxBuild2404.ps1 (host-side):
- Powers on template VM, waits for SSH readiness
- Copies Setup-LinuxBuild2404.sh into guest, runs it with sudo
- Validates setup result (all tools in PATH, dirs, sshd, swap off)
- Takes snapshot BaseClean-Linux (cold, VM shut down before snapshot)
- -NoMingw switch to skip mingw-w64 cross-compiler installation
Setup-LinuxBuild2404.sh (guest-side):
- apt update/upgrade, build-essential, gcc, g++, clang, cmake, ninja
- python3, pip3, venv; git, p7zip-full, curl, wget, ca-certificates
- Optional .NET SDK (--dotnet), mingw-w64 cross-compiler (--no-mingw to skip)
- /opt/ci/{build,output,scripts,cache} dirs (ci_build ownership)
- SSH hardening (PasswordAuthentication no, PubkeyAuthentication yes)
- Swap disable (swapoff -a + fstab), MAC-agnostic netplan for linked clones
- ci-report-ip systemd service (guestinfo.ci-ip reporter)
- apt auto-update disabled; full validation gate at end
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env bash
|
||||
# Setup-LinuxBuild2404.sh
|
||||
# Guest-side CI toolchain setup for Ubuntu 24.04 LTS build template.
|
||||
# Run as ci_build (passwordless sudo) via: sudo /tmp/Setup-LinuxBuild2404.sh
|
||||
# Called by: template/Prepare-LinuxBuild2404.ps1 (host-side)
|
||||
#
|
||||
# Usage:
|
||||
# sudo /tmp/Setup-LinuxBuild2404.sh [--skip-update] [--dotnet] [--mingw]
|
||||
#
|
||||
# Options:
|
||||
# --skip-update Skip apt update + upgrade (faster re-runs)
|
||||
# --dotnet Install .NET SDK 10.0 (optional, adds ~500 MB)
|
||||
# --no-mingw Skip mingw-w64 cross-compiler (installed by default)
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 All steps passed, system ready for snapshot
|
||||
# 1 One or more steps failed (output shows which step and why)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# --- Option parsing ---
|
||||
SKIP_UPDATE=0
|
||||
INSTALL_DOTNET=0
|
||||
INSTALL_MINGW=1
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--skip-update) SKIP_UPDATE=1 ;;
|
||||
--dotnet) INSTALL_DOTNET=1 ;;
|
||||
--no-mingw) INSTALL_MINGW=0 ;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# --- Assert helper ---
|
||||
STEP=0
|
||||
|
||||
assert_step() {
|
||||
local desc="$1"
|
||||
shift
|
||||
STEP=$((STEP + 1))
|
||||
if "$@"; then
|
||||
echo " [OK] [Step ${STEP}] ${desc}"
|
||||
else
|
||||
echo " [FAIL] [Step ${STEP}] ${desc}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Step 1: apt update + upgrade ---
|
||||
echo ""
|
||||
echo "=== Step 1: apt update + upgrade ==="
|
||||
if [ "${SKIP_UPDATE:-0}" = "0" ]; then
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y -qq
|
||||
assert_step "apt update + upgrade completed" true
|
||||
else
|
||||
echo " [SKIP] --skip-update flag set"
|
||||
fi
|
||||
|
||||
# --- Step 2: build essentials ---
|
||||
echo ""
|
||||
echo "=== Step 2: build essentials ==="
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
|
||||
build-essential gcc g++ clang \
|
||||
make cmake ninja-build \
|
||||
pkg-config autoconf automake libtool
|
||||
assert_step "gcc available" command -v gcc
|
||||
assert_step "g++ available" command -v g++
|
||||
assert_step "clang available" command -v clang
|
||||
assert_step "cmake available" command -v cmake
|
||||
assert_step "make available" command -v make
|
||||
|
||||
# --- Step 3: Python ---
|
||||
echo ""
|
||||
echo "=== Step 3: Python ==="
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq python3 python3-pip python3-venv
|
||||
sudo ln -sf /usr/bin/python3 /usr/local/bin/python
|
||||
assert_step "python3 available" command -v python3
|
||||
assert_step "pip3 available" command -v pip3
|
||||
|
||||
# --- Step 4: Tier-1 toolchain ---
|
||||
echo ""
|
||||
echo "=== Step 4: Tier-1 toolchain (git, 7-zip, curl, wget) ==="
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq git p7zip-full curl wget ca-certificates
|
||||
assert_step "git available" command -v git
|
||||
assert_step "7z available" command -v 7z
|
||||
assert_step "curl available" command -v curl
|
||||
|
||||
# --- Step 4b: .NET SDK (optional) ---
|
||||
echo ""
|
||||
echo "=== Step 4b: .NET SDK (optional) ==="
|
||||
if [ "${INSTALL_DOTNET:-0}" = "1" ]; then
|
||||
curl -fsSL https://dotnet.microsoft.com/download/dotnet/scripts/v1/dotnet-install.sh \
|
||||
| sudo bash -s -- --channel 10.0 --install-dir /usr/local/dotnet
|
||||
sudo ln -sf /usr/local/dotnet/dotnet /usr/local/bin/dotnet
|
||||
assert_step "dotnet available" command -v dotnet
|
||||
else
|
||||
echo " [SKIP] --dotnet not specified"
|
||||
fi
|
||||
|
||||
# --- Step 4c: mingw-w64 cross-compiler (default ON: required by ns7zip Linux build) ---
|
||||
echo ""
|
||||
echo "=== Step 4c: mingw-w64 cross-compiler ==="
|
||||
if [ "$INSTALL_MINGW" = "1" ]; then
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
|
||||
binutils-mingw-w64-i686 binutils-mingw-w64-x86-64 \
|
||||
gcc-mingw-w64-i686 gcc-mingw-w64-x86-64 \
|
||||
g++-mingw-w64-i686 g++-mingw-w64-x86-64
|
||||
assert_step "i686-w64-mingw32-gcc available" command -v i686-w64-mingw32-gcc
|
||||
assert_step "x86_64-w64-mingw32-gcc available" command -v x86_64-w64-mingw32-gcc
|
||||
assert_step "i686-w64-mingw32-g++ available" command -v i686-w64-mingw32-g++
|
||||
assert_step "x86_64-w64-mingw32-g++ available" command -v x86_64-w64-mingw32-g++
|
||||
else
|
||||
echo " [SKIP] --no-mingw passed, skipping mingw-w64"
|
||||
fi
|
||||
|
||||
# --- Step 5: CI directories + sudo group ---
|
||||
echo ""
|
||||
echo "=== Step 5: CI directories ==="
|
||||
sudo mkdir -p /opt/ci/{build,output,scripts,cache}
|
||||
sudo chown -R ci_build:ci_build /opt/ci
|
||||
# Ensure ci_build is in the sudo group (belt-and-suspenders alongside sudoers.d entry)
|
||||
sudo usermod -aG sudo ci_build
|
||||
assert_step "/opt/ci/build exists" test -d /opt/ci/build
|
||||
assert_step "/opt/ci/output exists" test -d /opt/ci/output
|
||||
assert_step "/opt/ci/scripts exists" test -d /opt/ci/scripts
|
||||
assert_step "/opt/ci/cache exists" test -d /opt/ci/cache
|
||||
assert_step "ci_build owns /opt/ci" test "$(stat -c '%U' /opt/ci)" = "ci_build"
|
||||
|
||||
# --- Step 6: SSH hardening ---
|
||||
echo ""
|
||||
echo "=== Step 6: SSH hardening ==="
|
||||
sudo sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
|
||||
sudo sed -i 's/^#*PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config
|
||||
sudo systemctl restart ssh
|
||||
assert_step "PasswordAuthentication no in sshd_config" grep -q "^PasswordAuthentication no" /etc/ssh/sshd_config
|
||||
assert_step "sshd service active" systemctl is-active ssh
|
||||
|
||||
# --- Step 7: disable swap ---
|
||||
echo ""
|
||||
echo "=== Step 7: disable swap ==="
|
||||
sudo swapoff -a
|
||||
sudo sed -i '/ swap / s/^/#/' /etc/fstab
|
||||
assert_step "no active swap" bash -c '[ -z "$(swapon --show)" ]'
|
||||
|
||||
# --- Step 7a: MAC-agnostic netplan for linked clones ---
|
||||
# Cloud-init generates /etc/netplan/50-cloud-init.yaml with the template's MAC
|
||||
# pinned via `match: macaddress: ...`. When VMware creates a linked clone with
|
||||
# a new MAC, that match fails and the clone has no network. We add a second
|
||||
# netplan file that matches any en* interface by name, so any clone (any MAC)
|
||||
# gets DHCP. The two files are merged; if 50-cloud-init also matches, fine —
|
||||
# we just guarantee DHCP works regardless.
|
||||
echo ""
|
||||
echo "=== Step 7a: MAC-agnostic netplan for clones ==="
|
||||
sudo tee /etc/netplan/99-ci-dhcp-allnics.yaml > /dev/null << 'EONETPLAN'
|
||||
network:
|
||||
version: 2
|
||||
ethernets:
|
||||
ci-all-en:
|
||||
match:
|
||||
name: "en*"
|
||||
dhcp4: true
|
||||
dhcp6: false
|
||||
EONETPLAN
|
||||
sudo chmod 600 /etc/netplan/99-ci-dhcp-allnics.yaml
|
||||
sudo chown root:root /etc/netplan/99-ci-dhcp-allnics.yaml
|
||||
sudo netplan generate
|
||||
assert_step "99-ci-dhcp-allnics.yaml exists" test -f /etc/netplan/99-ci-dhcp-allnics.yaml
|
||||
|
||||
# --- Step 7b: CI IP reporter via VMware guestinfo ---
|
||||
# The host reads the guest IP via `vmrun readVariable guestVar ci-ip`.
|
||||
# IMPORTANT: vmware-rpctool sets 'guestinfo.ci-ip' but vmrun reads it as just
|
||||
# 'ci-ip' under the guestVar namespace (the 'guestinfo.' prefix is implicit).
|
||||
# This is the official VMware VMCI channel — works even when TCP/IP is not yet
|
||||
# fully initialised on the host. Used as primary; getGuestIPAddress is fallback.
|
||||
echo ""
|
||||
echo "=== Step 7b: CI IP reporter ==="
|
||||
sudo tee /usr/local/bin/ci-report-ip.sh > /dev/null << 'EOSCRIPT'
|
||||
#!/bin/bash
|
||||
# Writes the VM's primary IPv4 to VMware guestinfo.ci-ip on every boot.
|
||||
# Called by ci-report-ip.service.
|
||||
# Retries for up to 60s to tolerate open-vm-tools re-init after UUID change
|
||||
# (linked clones get a new UUID — vmware-rpctool may fail briefly on first boot).
|
||||
for i in $(seq 1 30); do
|
||||
IP=$(ip -4 route get 1.0.0.0 2>/dev/null \
|
||||
| awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' \
|
||||
| head -1)
|
||||
if [ -n "$IP" ]; then
|
||||
if /usr/bin/vmware-rpctool "info-set guestinfo.ci-ip $IP" 2>/dev/null; then
|
||||
echo "ci-report-ip: set guestinfo.ci-ip=$IP (attempt $i)" | systemd-cat -t ci-report-ip
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "ci-report-ip: failed to set guestinfo.ci-ip after 30 attempts" | systemd-cat -t ci-report-ip
|
||||
exit 1
|
||||
EOSCRIPT
|
||||
sudo chmod +x /usr/local/bin/ci-report-ip.sh
|
||||
|
||||
sudo tee /etc/systemd/system/ci-report-ip.service > /dev/null << 'EOSERVICE'
|
||||
[Unit]
|
||||
Description=Report CI VM IP to VMware host via guestinfo
|
||||
After=open-vm-tools.service network.target
|
||||
Wants=open-vm-tools.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/ci-report-ip.sh
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=120
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOSERVICE
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable ci-report-ip.service
|
||||
assert_step "ci-report-ip.service enabled" systemctl is-enabled ci-report-ip.service
|
||||
|
||||
# --- Step 8: disable apt auto-update ---
|
||||
echo ""
|
||||
echo "=== Step 8: disable apt auto-update ==="
|
||||
sudo systemctl mask apt-daily.service apt-daily-upgrade.service \
|
||||
apt-daily.timer apt-daily-upgrade.timer 2>/dev/null || true
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get remove -y unattended-upgrades 2>/dev/null || true
|
||||
assert_step "apt-daily.timer masked or inactive" bash -c \
|
||||
'systemctl is-enabled apt-daily.timer 2>/dev/null | grep -qE "masked|disabled" || true'
|
||||
|
||||
# --- Step 9: cleanup pre-snapshot ---
|
||||
echo ""
|
||||
echo "=== Step 9: cleanup pre-snapshot ==="
|
||||
sudo apt-get clean
|
||||
sudo rm -rf /var/lib/apt/lists/*
|
||||
sudo journalctl --vacuum-time=1d 2>/dev/null || true
|
||||
sudo rm -rf /tmp/* /var/tmp/* 2>/dev/null || true
|
||||
history -c 2>/dev/null || true
|
||||
cat /dev/null > ~/.bash_history 2>/dev/null || true
|
||||
assert_step "apt cache cleaned" bash -c '[ -z "$(ls /var/cache/apt/archives/*.deb 2>/dev/null)" ]'
|
||||
|
||||
# --- Final Gate: pre-snapshot validation ---
|
||||
echo ""
|
||||
echo "=== Final Gate: pre-snapshot validation ==="
|
||||
|
||||
for tool in gcc g++ clang cmake make git 7z curl python3 pip3; do
|
||||
assert_step "${tool} in PATH" command -v "${tool}"
|
||||
done
|
||||
|
||||
for d in /opt/ci/build /opt/ci/output /opt/ci/scripts /opt/ci/cache; do
|
||||
assert_step "${d} exists" test -d "${d}"
|
||||
done
|
||||
assert_step "ci_build owns /opt/ci" test "$(stat -c '%U' /opt/ci)" = "ci_build"
|
||||
|
||||
assert_step "no active swap" bash -c '[ -z "$(swapon --show)" ]'
|
||||
|
||||
assert_step "sshd active" systemctl is-active ssh
|
||||
assert_step "PasswordAuthentication no" grep -q "^PasswordAuthentication no" /etc/ssh/sshd_config
|
||||
|
||||
assert_step "root fs >= 38 GB" bash -c \
|
||||
'df -BG / | awk '\''NR==2{gsub("G","",$2); if ($2+0 < 38) {print "FAIL: root fs too small ("$2"G)"; exit 1}}'\'''
|
||||
|
||||
assert_step "/tmp writable" bash -c 'touch /tmp/.ci_tmpcheck && rm /tmp/.ci_tmpcheck'
|
||||
assert_step "ci-report-ip.service enabled" systemctl is-enabled ci-report-ip.service
|
||||
|
||||
echo ""
|
||||
echo "=== Setup-LinuxBuild2404.sh complete ==="
|
||||
echo " All steps passed. VM is ready for BaseClean-Linux snapshot."
|
||||
echo " Run Prepare-LinuxBuild2404.ps1 --Step7Shutdown + snapshot from host."
|
||||
Reference in New Issue
Block a user