use isoloom_plan::Environment;
use isoloom_plan::lifecycle::{RunKind, Step};
use isoloom_spec::shell::quote;
use crate::configure::{self, Configured, shell};
use crate::network::{ipv6, more_addresses};
use crate::{constraints, powershell, secrets};
mod disks;
mod files;
mod network;
mod process;
mod settings;
mod shared_volume;
mod wait;
pub const PROJECT_DIR: &str = "/opt/isoloom";
pub const PROJECT_ARCHIVE: &str = "/tmp/isoloom-project.tgz";
pub const INPUTS_FILE: &str = "/tmp/isoloom-inputs.env";
pub const KEPT_INPUTS_FILE: &str = "/var/lib/isoloom/inputs.env";
pub const CLOUD_INIT_WAIT: &str = "cloud-init status --wait >/dev/null 2>&1 || true";
pub const PUBLISHED_FROM_OUTSIDE: &str = r"cat > /etc/systemd/system/isoloom-published.service <<'UNIT'
[Unit]
Description=Isoloom: published ports reach containers of networks without a default route
After=docker.service
Wants=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh -c 'iptables -t nat -C POSTROUTING -m conntrack --ctstate DNAT -o br-+ -j MASQUERADE 2>/dev/null || iptables -t nat -I POSTROUTING -m conntrack --ctstate DNAT -o br-+ -j MASQUERADE'
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable --now isoloom-published.service
";
pub const DOCKER_VERSION: &str = "29.9.0";
pub const COMPOSE_VERSION: &str = "5.6.0";
pub const DOCKER_APT_KEY_SHA256: &str = "1500c1f56fa9e26b9b8f42452a553675796ade0807cdce11975eb98170b3a570";
pub const INSTALL_DOCKER: &str = r#"command -v docker >/dev/null || if command -v apt-get >/dev/null; then (
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
k=$(mktemp)
# shellcheck disable=SC1091,SC2154 # /etc/os-release is on the guest, not here.
_id=$(. /etc/os-release && printf %s "$ID")
# shellcheck disable=SC1091,SC2154
_cn=$(. /etc/os-release && printf %s "$VERSION_CODENAME")
curl -fsSL "https://download.docker.com/linux/$_id/gpg" -o "$k"
echo "1500c1f56fa9e26b9b8f42452a553675796ade0807cdce11975eb98170b3a570 $k" | sha256sum -c -
install -m 0644 "$k" /etc/apt/keyrings/docker.asc
rm -f "$k"
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$_id $_cn stable" > /etc/apt/sources.list.d/docker.list
apt-get update -qq
v=$(apt-cache madison docker-ce | awk '$3 ~ /(^|:)29[.]9[.]0/ { print $3; exit }')
[ -n "$v" ] || { echo "docker-ce 29.9.0 is not in Docker's apt repository" >&2; exit 1; }
apt-get install -y -qq --allow-downgrades "docker-ce=$v" "docker-ce-cli=$v" containerd.io docker-buildx-plugin "docker-compose-plugin=5.6.0-*"
)
else
curl -fsSL https://get.docker.com | VERSION=29.9.0 sh
fi"#;
const WINDOWS_PROJECT_DIR: &str = "C:\\isoloom";
const PKG: &str = "pkg() { if command -v apt-get >/dev/null; then apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \"$@\"; elif command -v dnf >/dev/null; then dnf install -y -q \"$@\"; else yum install -y -q \"$@\"; fi; }";
const BOOT_ID: &str = "/proc/sys/kernel/random/boot_id";
#[must_use]
pub fn linux(env: &Environment, machine: &str, step: &Step) -> Option<String> {
match step {
Step::Prelude => constraints::linux_prelude(env.spec()),
Step::DiskGrow => Some(disks::GROW_ROOT_SH.to_owned()),
Step::Disks { disks } => disks::linux(disks),
Step::Settings => env.spec().machines.get(machine).and_then(settings::linux),
Step::Hosts { entries } => network::hosts(entries),
Step::Routes { routes, default } => network::routes(routes, *default),
Step::Ipv6 { addresses, routes } => Some(ipv6::linux(addresses, routes)),
Step::MoreAddresses { addresses } => Some(more_addresses::linux(addresses)),
Step::Trunks => network::trunks(env, machine),
Step::Volumes { paths } => (!paths.is_empty()).then(|| format!("mkdir -p {}\n", paths.iter().map(|p| quote(p)).collect::<Vec<_>>().join(" "))),
Step::SharedVolume { volume, path, role } => Some(shared_volume::linux(volume, path, role)),
Step::Project => Some(format!(
"mkdir -p {PROJECT_DIR} && tar -xzf {PROJECT_ARCHIVE} -C {PROJECT_DIR} --strip-components=1 && rm -f {PROJECT_ARCHIVE}\n"
)),
Step::Files { files } => Some(files::linux(files)),
Step::Secrets => env.machines.get(machine).map(|mp| secrets::step(machine, &mp.secrets)),
Step::Inputs { persist } => persist.then(|| format!("mkdir -p /var/lib/isoloom && install -m 600 {INPUTS_FILE} {KEPT_INPUTS_FILE}\n")),
Step::WaitFor { machine: dep, ports } => Some(wait::linux(dep, ports)),
Step::Run { path, kind, creates } => run(env, machine, path, *kind).map(|line| guarded(line, creates.as_deref())),
Step::Configure { resources, fresh } => Configured::of(env, machine).map(|c| shell::script(&c, resources, *fresh)),
Step::Reboot => Some(format!("{}(sleep 2 && reboot) >/dev/null 2>&1 &\n", record_boot())),
Step::Egress => Some(network::egress(env)),
Step::Tc => network::tc(env, machine),
Step::Command => process::command(env, machine),
Step::Setup { paths } => process::setup(env, machine, paths),
Step::Ready => Some(format!(
"{}mkdir -p /var/lib/isoloom && echo ready > /var/lib/isoloom/ready\n",
process::healthy(env, machine).unwrap_or_default()
)),
}
}
#[must_use]
pub fn windows(env: &Environment, machine: &str, step: &Step) -> Option<String> {
match step {
Step::DiskGrow => Some(disks::GROW_ROOT_PS1.to_owned()),
Step::Disks { disks } => disks::windows(disks),
Step::Settings => env.spec().machines.get(machine).and_then(settings::windows),
Step::Hosts { entries } => network::windows_hosts(entries),
Step::Routes { routes, .. } => network::windows_routes(routes),
Step::Ipv6 { addresses, routes } => Some(ipv6::windows(addresses, routes)),
Step::MoreAddresses { addresses } => Some(more_addresses::windows(addresses)),
Step::Project => Some(format!(
"$archive = 'C:\\Windows\\Temp\\isoloom-project.tgz'; $dest = '{WINDOWS_PROJECT_DIR}'; $strip = 1\n{}",
powershell::UNTAR
)),
Step::Files { files } => Some(files::windows(files)),
Step::Secrets => env.machines.get(machine).map(|mp| secrets::windows::step(machine, &mp.secrets)),
Step::WaitFor { machine: dep, ports } => wait::windows(dep, ports),
Step::Run {
path,
kind: RunKind::PowerShell,
creates,
} => {
let run = format!(
"Set-Location {WINDOWS_PROJECT_DIR}; & powershell -NoProfile -ExecutionPolicy Bypass -File {}; if ($LASTEXITCODE) {{ exit $LASTEXITCODE }}",
powershell::quote(path)
);
Some(match creates {
Some(done) => format!("if (-not (Test-Path -LiteralPath {})) {{ {run} }}", powershell::quote(done)),
None => run,
})
}
Step::Configure { resources, fresh } => Configured::of(env, machine).map(|c| configure::powershell::script(&c, resources, *fresh)),
Step::Command => process::windows_command(env, machine),
Step::Setup { paths } => Some(process::windows_setup(paths)),
Step::Ready => Some(format!(
"{}New-Item -ItemType Directory -Force {WINDOWS_PROJECT_DIR} | Out-Null; Set-Content {WINDOWS_PROJECT_DIR}\\ready ready",
process::windows_healthy(env, machine).unwrap_or_default()
)),
Step::Prelude
| Step::Trunks
| Step::Volumes { .. }
| Step::SharedVolume { .. }
| Step::Inputs { .. }
| Step::Run { .. }
| Step::Reboot
| Step::Egress
| Step::Tc => None,
}
}
fn run(env: &Environment, machine: &str, path: &str, kind: RunKind) -> Option<String> {
let inputs = inputs_file(env, machine).map(|f| format!("set -a; . {f}; set +a; ")).unwrap_or_default();
match kind {
RunKind::PowerShell => None,
RunKind::Shell => Some(format!("cd {PROJECT_DIR} && sh -c {}\n", quote(&format!("{inputs}sh {path}")))),
RunKind::Playbook => Some(format!(
"{PKG}\ncommand -v ansible-playbook >/dev/null || pkg ansible-core\ncd {PROJECT_DIR} && sh -c {}\n",
quote(&format!("{inputs}ansible-playbook -c local -i {machine}, {path}"))
)),
}
}
fn guarded(line: String, creates: Option<&str>) -> String {
match creates {
Some(path) => format!("[ -e {} ] || {{ {} }}\n", quote(path), line.trim_end()),
None => line,
}
}
fn inputs_file(env: &Environment, machine: &str) -> Option<&'static str> {
let m = env.machines.get(machine).filter(|m| !m.inputs.is_empty())?;
let kept = m.lifecycle.iter().any(|s| matches!(s, Step::Inputs { persist: true }));
Some(if kept { KEPT_INPUTS_FILE } else { INPUTS_FILE })
}
#[must_use]
pub fn record_boot() -> String {
format!("mkdir -p /var/lib/isoloom && cp {BOOT_ID} /var/lib/isoloom/boot-id\n")
}
#[must_use]
pub fn reboot_check() -> String {
format!("if [ \"$(cat {BOOT_ID})\" = \"$(cat /var/lib/isoloom/boot-id)\" ]; then echo 'the reboot step: the machine did not restart' >&2; exit 1; fi\n")
}
#[cfg(test)]
mod tests;