Skip to main content

scripts/incus_integration_vm.sh

#!/usr/bin/env sh
set -eu

INSTANCE_NAME="${HOSTKIT_INCUS_INSTANCE:-${HOSTKIT_INCUS_VM:-hostkit-test}}"
IMAGE="${HOSTKIT_INCUS_IMAGE:-images:ubuntu/24.04}"
TYPE="${HOSTKIT_INCUS_TYPE:-container}"
INCUS_BIN="${INCUS:-incus}"
INCUS_SUDO="${HOSTKIT_INCUS_SUDO:-false}"
PUBKEY="${HOSTKIT_SSH_PUBLIC_KEY:-$HOME/.ssh/id_rsa.pub}"

usage() {
  cat >&2 <<EOF
Usage: $0 COMMAND

Commands:
  install       Install Incus with apt if missing
  init          Initialize Incus with --minimal if needed
  ensure        Install, initialize, create/start the test instance, and install SSH
  create        Create/start the test instance and install SSH
  ip            Print the instance IP address
  ssh-config    Print an OpenSSH config entry for the instance
  destroy       Delete the test instance
  status        Show instance status

Environment:
  HOSTKIT_INCUS_INSTANCE    instance name (default: hostkit-test)
  HOSTKIT_INCUS_VM          legacy fallback for HOSTKIT_INCUS_INSTANCE
  HOSTKIT_INCUS_IMAGE       image alias (default: images:ubuntu/24.04)
  HOSTKIT_INCUS_TYPE        container or vm (default: container)
  HOSTKIT_INCUS_SUDO        run incus through sudo: true/false (default: false)
  HOSTKIT_SSH_PUBLIC_KEY    public key to authorize for root SSH
  INCUS                     incus executable (default: incus)
EOF
}

incus_cmd() {
  if [ "$INCUS_SUDO" = "true" ]; then
    sudo "$INCUS_BIN" "$@"
  else
    "$INCUS_BIN" "$@"
  fi
}

need_incus() {
  if ! command -v "$INCUS_BIN" >/dev/null 2>&1; then
    echo "incus not found. Run: $0 install" >&2
    exit 127
  fi
}

install_incus() {
  if command -v "$INCUS_BIN" >/dev/null 2>&1; then
    return 0
  fi

  sudo apt-get update
  sudo apt-get install -y incus
}

init_incus() {
  need_incus

  if incus_cmd storage show default >/dev/null 2>&1 && \
    incus_cmd profile show default 2>/dev/null | grep -q "pool: default"; then
    return 0
  fi

  incus_cmd admin init --minimal
}

instance_exists() {
  incus_cmd info "$INSTANCE_NAME" >/dev/null 2>&1
}

create_instance() {
  need_incus

  if [ ! -r "$PUBKEY" ]; then
    echo "public key not readable: $PUBKEY" >&2
    exit 1
  fi

  if ! instance_exists; then
    echo "[hostkit:incus] launch $TYPE $INSTANCE_NAME from $IMAGE" >&2
    case "$TYPE" in
      container)
        incus_cmd launch "$IMAGE" "$INSTANCE_NAME"
        ;;
      vm)
        incus_cmd launch "$IMAGE" "$INSTANCE_NAME" --vm
        ;;
      *)
        echo "unsupported HOSTKIT_INCUS_TYPE=$TYPE, expected container or vm" >&2
        exit 1
        ;;
    esac
  else
    echo "[hostkit:incus] start existing $INSTANCE_NAME" >&2
    incus_cmd start "$INSTANCE_NAME" >/dev/null 2>&1 || true
  fi

  wait_ready
  install_ssh
}

wait_ready() {
  echo "[hostkit:incus] wait for instance exec readiness" >&2
  i=0
  while [ "$i" -lt 120 ]; do
    if incus_cmd exec "$INSTANCE_NAME" -- true >/dev/null 2>&1; then
      break
    fi
    if [ $((i % 10)) -eq 0 ]; then
      echo "[hostkit:incus] still waiting for $INSTANCE_NAME (${i}s)" >&2
    fi
    i=$((i + 1))
    sleep 1
  done

  if [ "$i" -ge 120 ]; then
    echo "instance $INSTANCE_NAME did not become ready" >&2
    exit 1
  fi

  echo "[hostkit:incus] wait for cloud-init (if present)" >&2
  incus_cmd exec "$INSTANCE_NAME" -- cloud-init status --wait >/dev/null 2>&1 || true
  echo "[hostkit:incus] instance ready" >&2
}

install_ssh() {
  echo "[hostkit:incus] apt-get update" >&2
  incus_cmd exec "$INSTANCE_NAME" -- env DEBIAN_FRONTEND=noninteractive apt-get update
  echo "[hostkit:incus] apt-get install openssh-server sudo ca-certificates curl git" >&2
  incus_cmd exec "$INSTANCE_NAME" -- env DEBIAN_FRONTEND=noninteractive apt-get install -y openssh-server sudo ca-certificates curl git
  incus_cmd exec "$INSTANCE_NAME" -- mkdir -p /root/.ssh
  incus_cmd exec "$INSTANCE_NAME" -- chmod 700 /root/.ssh
  incus_cmd file push "$PUBKEY" "$INSTANCE_NAME/root/.ssh/authorized_keys" --uid 0 --gid 0 --mode 0600

  if incus_cmd exec "$INSTANCE_NAME" -- systemctl is-active --quiet ssh >/dev/null 2>&1; then
    echo "[hostkit:incus] ssh already active" >&2
    return 0
  fi

  echo "[hostkit:incus] start ssh" >&2
  incus_cmd exec "$INSTANCE_NAME" -- sh -c 'timeout 30s service ssh start >/dev/null 2>&1 || timeout 30s systemctl start ssh >/dev/null 2>&1 || true'

  if ! incus_cmd exec "$INSTANCE_NAME" -- systemctl is-active --quiet ssh >/dev/null 2>&1; then
    echo "ssh did not become active" >&2
    exit 1
  fi
}

instance_ip() {
  need_incus

  ip=$(incus_cmd list "$INSTANCE_NAME" -c 4 --format csv | tr ' ' '\n' | awk '/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ { print; exit }')

  if [ -z "${ip:-}" ]; then
    echo "could not determine IPv4 address for $INSTANCE_NAME" >&2
    exit 1
  fi

  printf '%s\n' "$ip"
}

ssh_config() {
  ip=$(instance_ip)
  cat <<EOF
Host $INSTANCE_NAME
  HostName $ip
  User root
  StrictHostKeyChecking accept-new
EOF
}

destroy_instance() {
  need_incus
  incus_cmd delete "$INSTANCE_NAME" --force
}

status() {
  need_incus
  incus_cmd list "$INSTANCE_NAME"
}

ensure() {
  install_incus
  init_incus
  create_instance
}

case "${1:-}" in
  install) install_incus ;;
  init) init_incus ;;
  ensure) ensure ;;
  create) create_instance ;;
  ip) instance_ip ;;
  ssh-config) ssh_config ;;
  destroy) destroy_instance ;;
  status) status ;;
  -h|--help|help) usage ;;
  *) usage; exit 2 ;;
esac