Files
local-ci-cd-system/template/Install-CIToolchain-Linux2404.sh
T
Simone 41a0a113df feat(templates): install ci-static-ip/ci-report-ip via Prepare+Install scripts
Windows (Prepare-WinBuild2025/2022): read ci-static-ip.ps1 from
guest-setup/windows/ on the host, push content over WinRM, register the
CI-StaticIp scheduled task (AtStartup/SYSTEM). Adds StaticIpTask and
StaticIpScript post-setup assertions.

Linux (Install-CIToolchain-Linux2404.sh): embed ci-report-ip.sh and
ci-report-ip.service content inline via heredoc tee, replacing the broken
cp-from-relative-path approach (the script runs inside the VM where the
host-side guest-setup/ directory does not exist).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 00:37:07 +02:00

331 lines
13 KiB
Bash

#!/usr/bin/env bash
# Install-CIToolchain-Linux2404.sh
# Guest-side CI toolchain setup for Ubuntu 24.04 LTS build template.
# Run as ci_build (passwordless sudo) via: sudo /tmp/Install-CIToolchain-Linux2404.sh
# Called by: template/Prepare-LinuxBuild2404.ps1 (host-side)
#
# Usage:
# sudo /tmp/Install-CIToolchain-Linux2404.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 ---
# Supports two modes:
# Static (ip_pool): reads guestinfo.ip-assignment injected by the host
# orchestrator, applies static NIC config, reports back.
# DHCP fallback: waits for DHCP address, reports via guestinfo.ci-ip.
# Content embedded inline — this script runs inside the VM where the host-side
# template/guest-setup/linux/ directory is not present.
echo ""
echo "=== Step 7b: CI IP reporter (with static IP support) ==="
sudo tee /usr/local/bin/ci-report-ip.sh > /dev/null << 'EOREPORTIP'
#!/bin/bash
# CI VM IP reporter — called by ci-report-ip.service at boot.
#
# Supports two modes:
# Static (ip_pool): reads guestinfo.ip-assignment injected by the host
# orchestrator, applies static NIC config, reports back.
# DHCP fallback: waits for DHCP address, reports via guestinfo.ci-ip.
#
# The VMCI channel (vmware-rpctool) does not require TCP/IP to be initialised —
# it communicates via the VMware virtual hardware interface. This means the
# static IP can be configured before network-online.target, so by the time SSH
# starts listening the NIC already has the correct address.
RPCTOOL=/usr/bin/vmware-rpctool
[ -x "$RPCTOOL" ] || exit 0
# ── Check for pre-assigned IP from orchestrator ───────────────────────────────
assigned_ip=$("$RPCTOOL" "info-get guestinfo.ip-assignment" 2>/dev/null | tr -d '[:space:]')
if [[ "$assigned_ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
netmask=$("$RPCTOOL" "info-get guestinfo.netmask" 2>/dev/null | tr -d '[:space:]')
gateway=$("$RPCTOOL" "info-get guestinfo.gateway" 2>/dev/null | tr -d '[:space:]')
# Resolve primary NIC (first non-loopback).
iface=$(ip -o link show | awk -F': ' '$2 != "lo" {print $2; exit}')
if [ -z "$iface" ]; then
echo "ci-report-ip: no non-loopback interface found" | systemd-cat -t ci-report-ip
exit 1
fi
# Compute prefix length from netmask (default /24).
prefix=24
if [ -n "$netmask" ]; then
prefix=$(python3 -c \
"import ipaddress; print(ipaddress.IPv4Network('0.0.0.0/$netmask',strict=False).prefixlen)" \
2>/dev/null) || prefix=24
fi
# Flush any existing config (DHCP or stale static) and apply.
ip addr flush dev "$iface" 2>/dev/null || true
ip addr add "${assigned_ip}/${prefix}" dev "$iface"
ip link set "$iface" up
if [[ "$gateway" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
ip route add default via "$gateway" dev "$iface" 2>/dev/null || true
fi
# Report IP back to host via guestinfo.ci-ip.
"$RPCTOOL" "info-set guestinfo.ci-ip $assigned_ip" 2>/dev/null
echo "ci-report-ip: applied static IP $assigned_ip/$prefix via guestinfo" \
| systemd-cat -t ci-report-ip
exit 0
fi
# ── Fallback: DHCP — wait for IP and report ───────────────────────────────────
# Retries for up to 60 s 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 "$RPCTOOL" "info-set guestinfo.ci-ip $IP" 2>/dev/null; then
echo "ci-report-ip: set guestinfo.ci-ip=$IP via DHCP (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
EOREPORTIP
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
# Run after open-vm-tools so vmware-rpctool is available.
# Do NOT add network-online.target — we want to run BEFORE the network stack
# settles so static IP is applied before SSH/other services start.
After=open-vm-tools.service
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 "=== Install-CIToolchain-Linux2404.sh complete ==="
echo " All steps passed. VM is ready for BaseClean-Linux snapshot."
echo " Run Prepare-LinuxBuild2404.ps1 --Step7Shutdown + snapshot from host."