setup.sh
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bash
# Host setup only. Application dependencies are built in the backend container.
set -Eeuo pipefail
export LC_ALL=C
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
DRY_RUN=false
ASSUME_YES=false
CHECK_ONLY=false
TARGET_USER=
DISTRO=
INIT_SYSTEM=
PHASE=preflight
ROOT=()
LIBVIRT_SOCKET=/var/run/libvirt/libvirt-sock
STORAGE_DIR=/var/lib/distribox
OPENRC_DIR=/etc/init.d
RUNIT_AVAILABLE=/etc/sv
RUNIT_ENABLED=/var/service
TUN_DEVICE=/dev/net/tun
usage() {
cat <<'EOF'
Usage: bash setup.sh [--dry-run] [--yes] [--user USER] [--check]
Prepare a Linux host for Distribox: install QEMU/libvirt, enable local services
and the default NAT network, create shared storage, and add user groups.
--dry-run Print the planned changes without sudo or host modifications.
--yes, -y Accept package-manager prompts (including Gentoo builds).
--user USER User receiving storage/libvirt access. Defaults to the invoking
user, or SUDO_USER under sudo. Required for a direct root login.
--check Diagnose an existing host without changing it.
--help, -h Show this help.
Supported: Debian/Ubuntu, Arch, Fedora/RHEL, openSUSE, Alpine, Gentoo, Void,
Solus, and NixOS (declarative flake module).
Install Docker Engine and Compose separately. On minimal Alpine, install Bash
first. Package managers keep their native prompts; APK applies changes directly.
Log out and back in after setup to apply all group memberships.
EOF
}
info() { printf '\n==> %s\n' "$*"; }
warn() { printf 'Warning: %s\n' "$*" >&2; }
die() { printf 'Error: %s\n' "$*" >&2; exit 1; }
on_error() {
printf '\nError during %s (line %s, exit %s): %s\n' "$PHASE" "$2" "$1" "$3" >&2
printf 'Resolve the error above, then rerun setup. Use --check to inspect the host.\n' >&2
exit "$1"
}
run() {
printf ' $'
printf ' %q' "$@"
printf '\n'
if ! "$DRY_RUN"; then "$@"; fi
}
run_root() { run "${ROOT[@]}" "$@"; }
query_root() { "${ROOT[@]}" "$@"; }
virsh_query() { query_root virsh --connect qemu:///system "$@"; }
virsh_run() { run_root virsh --connect qemu:///system "$@"; }
require_command() { command -v "$1" >/dev/null || die "Required command '$1' is missing. $2"; }
parse_args() {
while (($#)); do
case "$1" in
--dry-run) DRY_RUN=true ;;
--yes|-y) ASSUME_YES=true ;;
--check) CHECK_ONLY=true ;;
--user)
[[ $# -ge 2 && -n $2 && $2 != -* ]] || die '--user requires a username.'
TARGET_USER=$2
shift
;;
--help|-h) usage; exit 0 ;;
*) die "Unknown argument '$1'. Use --help for usage." ;;
esac
shift
done
if "$CHECK_ONLY" && "$DRY_RUN"; then die 'Use --check or --dry-run separately.'; fi
}
read_os_release() {
local release_file=${1:-/etc/os-release}
local ID='' ID_LIKE=''
[[ -r $release_file ]] || release_file=/usr/lib/os-release
[[ -r $release_file ]] || die 'Cannot read os-release; the Linux distribution is unknown.'
# os-release is an administrator-owned shell assignment file.
# shellcheck disable=SC1090
. "$release_file"
local candidate
for candidate in "${ID:-}" ${ID_LIKE:-}; do
case "$candidate" in
nixos) DISTRO=nixos; return ;;
debian|ubuntu) DISTRO=debian; return ;;
arch|archlinux) DISTRO=arch; return ;;
fedora|rhel|centos|rocky|almalinux) DISTRO=fedora; return ;;
opensuse*|suse|sles) DISTRO=opensuse; return ;;
alpine|gentoo|void|solus) DISTRO=$candidate; return ;;
esac
done
die "Unsupported distribution '${ID:-unknown}'. No changes made. Use --help for supported systems."
}
detect_platform() {
[[ $(uname -s) == Linux ]] || die 'Distribox needs a Linux host: its containers use host libvirt and KVM. Use a Linux machine or VM.'
[[ $(uname -m) == x86_64 ]] || die 'Distribox currently creates x86_64 VMs. Use an x86_64 Linux host; other host architectures need a separate emulator configuration.'
read_os_release
}
detect_init() {
if [[ -d /run/systemd/system ]]; then
INIT_SYSTEM=systemd
require_command systemctl 'Boot the host with systemd.'
systemctl show --property=Version --value >/dev/null 2>&1 || die 'Cannot reach the running systemd manager. Run setup on the host, outside a container/chroot.'
elif [[ -r /run/openrc/softlevel ]] && command -v rc-service >/dev/null && command -v rc-update >/dev/null; then
INIT_SYSTEM=openrc
elif [[ $DISTRO == void ]] && command -v sv >/dev/null && [[ -d /var/service ]]; then
INIT_SYSTEM=runit
else
die 'No supported running init system found (systemd, OpenRC, or Void runit). Run this on the host, outside a container/chroot.'
fi
}
select_user() {
TARGET_USER=${TARGET_USER:-${SUDO_USER:-$(id -un)}}
[[ $TARGET_USER != root ]] || die 'Choose a non-root operator with --user USER (or run setup as that user).'
[[ $TARGET_USER =~ ^[a-zA-Z_][a-zA-Z0-9_.-]*\$?$ ]] || die "Invalid username '$TARGET_USER'."
id "$TARGET_USER" >/dev/null 2>&1 || die "User '$TARGET_USER' does not exist. Create the operator account first."
[[ $(id -u "$TARGET_USER") != 0 ]] || die 'The operator account must have a non-root UID.'
}
prepare_privileges() {
if ((EUID != 0)); then
require_command sudo 'Install sudo or run sudo bash setup.sh --user USER.'
ROOT=(sudo)
if ! "$DRY_RUN"; then sudo -v; fi
fi
}
install_packages() {
PHASE='package installation'
info "Install host packages ($DISTRO)"
local yes=()
"$ASSUME_YES" && yes=(-y)
case "$DISTRO" in
debian)
require_command apt-get 'This host must provide apt-get.'
run_root apt-get update
run_root apt-get install "${yes[@]}" qemu-system-x86 qemu-utils libvirt-daemon-system libvirt-clients dnsmasq-base ovmf
;;
arch)
require_command pacman 'This host must provide pacman.'
yes=()
"$ASSUME_YES" && yes=(--noconfirm)
local packages=(libvirt qemu-desktop dnsmasq edk2-ovmf nftables)
# Keep an existing iptables provider; do not force a conflicting replacement.
if ! command -v iptables >/dev/null; then packages+=(iptables-nft); fi
# No database-only refresh: Arch does not support partial upgrades.
run_root pacman -S --needed "${yes[@]}" "${packages[@]}"
;;
fedora)
local manager=dnf
command -v dnf >/dev/null || manager=yum
require_command "$manager" 'Install dnf or yum first.'
run_root "$manager" install "${yes[@]}" qemu-kvm qemu-img libvirt-daemon-kvm libvirt-client libvirt-daemon-config-network dnsmasq edk2-ovmf
;;
opensuse)
require_command zypper 'This host must provide zypper.'
yes=()
"$ASSUME_YES" && yes=(--non-interactive)
run_root zypper "${yes[@]}" install qemu libvirt-daemon-qemu libvirt-client libvirt-daemon-config-network dnsmasq qemu-ovmf-x86_64
;;
alpine)
require_command apk 'This host must provide apk.'
run_root apk add libvirt-daemon libvirt-qemu libvirt-daemon-openrc qemu-system-x86_64 qemu-img qemu-modules ovmf dbus polkit
;;
gentoo)
require_command emerge 'This host must provide Portage.'
yes=(--ask)
"$ASSUME_YES" && yes=(--ask=n)
info 'If Portage requests USE changes, enable the requested flags in package.use and rerun; setup leaves Portage configuration to you.'
run_root emerge "${yes[@]}" --noreplace 'app-emulation/libvirt[libvirtd,qemu,virt-network,policykit]' 'app-emulation/qemu[vnc,qemu_softmmu_targets_x86_64]' sys-firmware/edk2 sys-apps/dbus
;;
void)
require_command xbps-install 'This host must provide XBPS.'
run_root xbps-install -S "${yes[@]}" libvirt qemu dbus polkit
;;
solus)
require_command eopkg 'This host must provide eopkg.'
run_root eopkg install "${yes[@]}" qemu libvirt dnsmasq
;;
esac
}
prepare_tun() {
PHASE='TUN kernel support'
info 'Ensure guest TAP devices work now and after reboot'
if [[ ! -c $TUN_DEVICE ]]; then
require_command modprobe 'Install kmod or enable TUN in the host kernel.'
run_root modprobe tun
if ! "$DRY_RUN" && [[ ! -c $TUN_DEVICE ]]; then
die "TUN is unavailable at $TUN_DEVICE after modprobe. Enable CONFIG_TUN in the host kernel; do not run setup inside a container."
fi
fi
run_root install -d -m 755 /etc/modules-load.d
if "$DRY_RUN"; then
info 'Write tun to /etc/modules-load.d/60-distribox.conf for future boots'
else
printf 'tun\n' | query_root tee /etc/modules-load.d/60-distribox.conf >/dev/null
fi
}
unit_exists() {
local state
state=$(systemctl show --property=LoadState --value "$1" 2>/dev/null) || return 1
[[ $state == loaded ]]
}
systemd_services() {
local service
run_root systemctl daemon-reload
if systemctl is-active --quiet libvirtd.service libvirtd.socket; then
run_root systemctl enable --now libvirtd.service
elif systemctl is-active --quiet virtqemud.service virtqemud.socket virtproxyd.socket || unit_exists virtqemud.socket; then
info 'Use modular libvirt with the compatibility socket required by Compose'
for service in virtlogd virtlockd virtqemud virtnetworkd virtstoraged virtnodedevd virtnwfilterd virtsecretd virtproxyd; do
if "$DRY_RUN" || unit_exists "$service.socket"; then
run_root systemctl enable --now "$service.socket"
elif [[ $service == virtqemud || $service == virtnetworkd || $service == virtproxyd ]]; then
die "Missing $service.socket; install the QEMU, network, and proxy libvirt drivers."
fi
done
elif "$DRY_RUN" || unit_exists libvirtd.service; then
run_root systemctl enable --now libvirtd.service
else
die 'No libvirt service units were installed. Check the package-manager output.'
fi
}
openrc_services() {
local service
for service in dbus virtlogd virtlockd libvirtd; do
if "$DRY_RUN" || [[ -x $OPENRC_DIR/$service ]]; then
run_root rc-update add "$service" default
run_root rc-service "$service" start
elif [[ $service == libvirtd || $service == virtlogd ]]; then
die "Missing OpenRC service '$service'. Install the distribution's libvirt OpenRC package."
fi
done
}
runit_services() {
local service
local services=(dbus virtlogd virtlockd libvirtd)
# Preserve a host already using modular daemons.
if [[ -e $RUNIT_ENABLED/virtqemud || -L $RUNIT_ENABLED/virtqemud ]]; then
services=(dbus virtlogd virtlockd virtqemud virtnetworkd virtstoraged virtnodedevd virtnwfilterd virtsecretd virtproxyd)
fi
for service in "${services[@]}"; do
local service_path=$RUNIT_AVAILABLE/$service
if [[ -e $RUNIT_ENABLED/$service || -L $RUNIT_ENABLED/$service ]]; then service_path=$RUNIT_ENABLED/$service; fi
if ! "$DRY_RUN" && [[ ! -d $service_path ]]; then die "Missing runit service $service_path (check for a dangling link)."; fi
if [[ -e $service_path/down ]]; then
die "$service is disabled by $service_path/down. Review and remove that marker before enabling it at boot."
fi
if [[ ! -e $RUNIT_ENABLED/$service && ! -L $RUNIT_ENABLED/$service ]]; then
run_root ln -s "$RUNIT_AVAILABLE/$service" "$RUNIT_ENABLED/$service"
fi
if ! "$DRY_RUN"; then
# runsvdir scans new links asynchronously; sv's -w does not wait for it.
local attempt
for ((attempt=0; attempt<30; attempt++)); do
if query_root test -p "$RUNIT_ENABLED/$service/supervise/ok"; then break; fi
sleep 1
done
((attempt < 30)) || die "runit did not supervise $service within 30 seconds. Check runsvdir and the enabled-service link."
fi
run_root sv -w 30 up "$RUNIT_ENABLED/$service"
done
}
enable_services() {
PHASE='service startup'
info "Enable local libvirt ($INIT_SYSTEM); existing guests are not restarted"
case "$INIT_SYSTEM" in
systemd) systemd_services ;;
openrc) openrc_services ;;
runit) runit_services ;;
esac
}
wait_for_libvirt() {
PHASE='libvirt readiness'
info 'Wait for the local libvirt API (up to 60 seconds)'
if "$DRY_RUN"; then return; fi
require_command virsh 'Install the distribution libvirt client package.'
require_command timeout 'Install coreutils or BusyBox timeout for bounded daemon checks.'
local attempt output=
for ((attempt=0; attempt<20; attempt++)); do
if [[ -S $LIBVIRT_SOCKET ]] && output=$(query_root timeout 2 virsh --connect qemu:///system list --all 2>&1); then return; fi
if ((attempt < 19)); then sleep 1; fi
done
# Keep the final daemon error visible, without an unbounded diagnostic query.
if [[ -n $output ]]; then printf '%s\n' "$output" >&2; fi
die "Libvirt readiness checks failed after the retry window (expected $LIBVIRT_SOCKET). Inspect the service logs before rerunning setup."
}
configure_network() {
PHASE='default NAT network'
info 'Ensure the default libvirt network exists, starts now, and starts at boot'
if "$DRY_RUN"; then
info 'If default is missing, define the bundled NAT network (virbr0, 192.168.122.0/24). Existing definitions are preserved.'
virsh_run net-define "$SCRIPT_DIR/setup/default-network.xml"
virsh_run net-autostart default
virsh_run net-start default
return
fi
# List first: a failed connection must never be mistaken for a missing network.
local networks active
networks=$(virsh_query net-list --all --name)
if ! printf '%s\n' "$networks" | grep -Fx default >/dev/null; then
virsh_run net-define "$SCRIPT_DIR/setup/default-network.xml"
fi
virsh_run net-autostart default
active=$(virsh_query net-list --name)
if ! printf '%s\n' "$active" | grep -Fx default >/dev/null; then virsh_run net-start default; fi
}
configure_groups() {
PHASE='operator groups'
info "Grant $TARGET_USER storage and libvirt administration access"
local group groups=(distribox libvirt)
if getent group kvm >/dev/null; then groups+=(kvm); fi
for group in "${groups[@]}"; do
if ! getent group "$group" >/dev/null; then
if command -v groupadd >/dev/null; then run_root groupadd "$group"
else run_root addgroup -S "$group"; fi
fi
if id -nG "$TARGET_USER" | tr ' ' '\n' | grep -Fxq "$group"; then continue; fi
if command -v usermod >/dev/null; then run_root usermod -aG "$group" "$TARGET_USER"
else run_root addgroup "$TARGET_USER" "$group"; fi
done
}
configure_storage() {
PHASE='shared storage'
info 'Prepare shared directories; existing VM files keep their ownership and permissions'
local directory
for directory in "$STORAGE_DIR" "$STORAGE_DIR/images" "$STORAGE_DIR/vms"; do
[[ ! -L $directory ]] || die "$directory is a symlink. Use a bind mount for custom storage; see the storage section in README.md."
done
run_root install -d -m 2775 -o root -g distribox "$STORAGE_DIR" "$STORAGE_DIR/images" "$STORAGE_DIR/vms"
# QEMU identities differ by distro. Libvirt manages individual disk ownership.
if command -v getenforce >/dev/null && [[ $(getenforce) != Disabled ]]; then
warn 'SELinux is enabled. Configure image-storage labels and backend access to the libvirt socket before starting VMs; see README.md.'
fi
}
configure_sysctl() {
PHASE='remote desktop TCP tuning'
info 'Keep TCP congestion windows across idle desktop sessions'
run_root install -d -m 755 /etc/sysctl.d
if "$DRY_RUN"; then
info 'Write net.ipv4.tcp_slow_start_after_idle = 0 to /etc/sysctl.d/60-distribox.conf'
else
printf 'net.ipv4.tcp_slow_start_after_idle = 0\n' | query_root tee /etc/sysctl.d/60-distribox.conf >/dev/null
fi
run_root sysctl -q -p /etc/sysctl.d/60-distribox.conf
}
check_host() {
PHASE='host verification'
info 'Verify the host connection and default network'
if "$DRY_RUN"; then
virsh_run list --all
return
fi
require_command virsh 'Install the libvirt client package.'
[[ -S $LIBVIRT_SOCKET ]] || die "The compatibility socket $LIBVIRT_SOCKET is missing; Compose cannot reach libvirt."
virsh_query list --all >/dev/null
virsh_query domcapabilities --arch x86_64 --machine pc --virttype qemu >/dev/null
[[ -c $TUN_DEVICE ]] || die "Missing TUN device $TUN_DEVICE. Enable the host's tun kernel module before starting guests."
local active
active=$(virsh_query net-list --name)
printf '%s\n' "$active" | grep -Fx default >/dev/null || die "The libvirt 'default' network is inactive. Inspect its definition with virsh net-dumpxml default, host routes, firewall, and daemon logs."
local directory
for directory in "$STORAGE_DIR" "$STORAGE_DIR/images" "$STORAGE_DIR/vms"; do
[[ -d $directory ]] || die "Missing storage directory $directory."
done
[[ -c /dev/kvm ]] || warn 'KVM is unavailable. Enable CPU virtualization/nested virtualization, or set VIRT_TYPE=qemu in .env for slower software emulation.'
if ! command -v docker >/dev/null || ! docker compose version >/dev/null 2>&1; then
warn 'Install Docker Engine and the Compose plugin separately before starting Distribox.'
elif ! docker info >/dev/null 2>&1; then
warn 'Docker is not reachable as the current user. Start Docker and arrange access before starting Compose.'
fi
}
nixos_instructions() {
cat <<'EOF'
NixOS hosts are configured declaratively. No host changes were made.
Add the Distribox flake input to your existing host flake:
inputs.distribox.url = "github:PoCInnovation/Distribox";
Add to your nixosSystem modules (with distribox in your outputs arguments):
distribox.nixosModules.default
{
services.distriboxHost.enable = true;
services.distriboxHost.users = [ "YOUR_USER" ];
virtualisation.docker.enable = true;
}
Review and apply your host configuration with nixos-rebuild switch --flake.
Log out and back in, then run: bash setup.sh --check
For a local checkout, use inputs.distribox.url = "path:/absolute/path/to/Distribox".
For a customized default network bridge, set services.distriboxHost.networkInterface
to that bridge's name. Keep your existing QEMU runtime identity and Docker policy.
EOF
}
main() {
trap 'on_error "$?" "$LINENO" "$BASH_COMMAND"' ERR
parse_args "$@"
detect_platform
if [[ $DISTRO == nixos ]] && ! "$CHECK_ONLY"; then nixos_instructions; return; fi
if "$CHECK_ONLY"; then
prepare_privileges
check_host
info 'Host checks passed. Docker/KVM warnings above still need attention.'
return
fi
select_user
detect_init
info "Distribox host setup: $DISTRO / $INIT_SYSTEM / operator $TARGET_USER"
if "$DRY_RUN"; then info 'Dry run: commands below are a plan; no changes or privilege prompts.'; fi
prepare_privileges
install_packages
prepare_tun
enable_services
wait_for_libvirt
configure_network
configure_groups
configure_storage
configure_sysctl
check_host
if "$DRY_RUN"; then
info 'Dry run complete. Service and network choices are checked again after installation.'
else
info 'Host setup complete. Log out and back in to apply all user group changes.'
printf 'Next: configure .env, then run docker compose --profile master up -d --build\n'
printf 'For a slave node use the dashboard setup guide.\n'
fi
}
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then main "$@"; fi