Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,47 @@ const stepVmbr1: Step = {
},
}

const BUILD_NET_SUBNET = '10.0.0.0/24'
const BUILD_NET_FW_COMMENT = 'cofoundry build network (packer HTTP)'

// When the Proxmox firewall is enabled, the host's PVEFW-INPUT chain drops the
// build VM's connection to packer's HTTP server (preseed/kickstart fetch) on
// vmbr1 — the build then hangs at "Waiting for SSH". A pve-firewall host rule is
// the correct fix: it survives reboots AND firewall reloads, unlike a post-up
// iptables rule which pve-firewall flushes whenever it recompiles. No-op when the
// firewall is disabled (nothing to open).
const stepBuildNetFirewall: Step = {
id: 'build-net-firewall',
label: 'allow build network through Proxmox firewall',
inScope: plan => plan.needBuildNet,
probe: async plan => {
const status = await sshCapture(
plan.target,
'pve-firewall status 2>/dev/null'
)
if (!/enabled/i.test(status.stdout)) {
return {
done: true,
note: 'Proxmox firewall disabled — no rule needed',
}
}
const rules = await sshCapture(
plan.target,
`pvesh get /nodes/$(hostname)/firewall/rules --output-format json 2>/dev/null`
)
return rules.stdout.includes('cofoundry build network')
? { done: true, note: 'host firewall rule already present' }
: { done: false, note: 'Proxmox firewall on — opening build net' }
},
apply: async plan => {
await remoteStreaming(
plan.target,
`pvesh create /nodes/$(hostname)/firewall/rules --action ACCEPT --type in --source ${BUILD_NET_SUBNET} --enable 1 --comment ${shellQuote(BUILD_NET_FW_COMMENT)}`
)
return { note: `allowed ${BUILD_NET_SUBNET} in (host firewall rule)` }
},
}

const stepDnsmasq: Step = {
id: 'dnsmasq',
label: 'install dnsmasq',
Expand Down Expand Up @@ -347,6 +388,7 @@ const ALL_STEPS: Step[] = [
stepAwscli,
stepIsoCache,
stepVmbr1,
stepBuildNetFirewall,
stepDnsmasq,
stepDnsmasqConf,
stepNetslotDir,
Expand Down
13 changes: 9 additions & 4 deletions src/build/remote.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { execa, ExecaError } from 'execa'
import { redactSensitive } from '../util.ts'

// Keep idle SSH sessions alive — and detect a dead peer — so a long quiet remote
// step (e.g. a multi-hundred-MB artifact upload in CF_UPLOAD_CMD) can't leave the
// build hanging forever on a half-open connection.
const SSH_OPTS = ['-o', 'ServerAliveInterval=15', '-o', 'ServerAliveCountMax=6']

// ── SIGINT cleanup ────────────────────────────────────────────────────────────

type KillableProc = { kill: (signal?: string) => boolean }
Expand All @@ -25,7 +30,7 @@ export const captureRemote = async (
cmd: string
): Promise<string> => {
try {
const { stdout } = await execa('ssh', [target, cmd], {
const { stdout } = await execa('ssh', [...SSH_OPTS, target, cmd], {
stdin: 'inherit',
stderr: 'inherit',
})
Expand All @@ -45,14 +50,14 @@ export const remoteStreaming = (
target: string,
cmd: string,
onLine?: (line: string) => void
): Promise<void> => streaming('ssh', [target, cmd], onLine)
): Promise<void> => streaming('ssh', [...SSH_OPTS, target, cmd], onLine)

// Allocates a PTY so remote programs (e.g. wget) detect a terminal and show
// their native progress bar rather than falling back to dot-style output.
export const remoteStreamingPty = (
target: string,
cmd: string
): Promise<void> => streaming('ssh', ['-t', '-t', target, cmd])
): Promise<void> => streaming('ssh', [...SSH_OPTS, '-t', '-t', target, cmd])

// Wget exit codes worth surfacing. See man wget(1) EXIT STATUS.
const WGET_EXIT: Record<number, string> = {
Expand All @@ -75,7 +80,7 @@ export const remoteWgetCapture = async (
onLine: (line: string) => void,
context?: { url?: string; what?: string }
): Promise<void> => {
const proc = execa('ssh', ['-t', '-t', target, `{ ${cmd}; } 2>&1`], {
const proc = execa('ssh', [...SSH_OPTS, '-t', '-t', target, `{ ${cmd}; } 2>&1`], {
stdin: 'pipe',
stdout: 'pipe',
stderr: 'ignore',
Expand Down
Loading