Ctrl K
ComunidadGuías…

install.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash
#
# BIND9 Web UI — API backend installer.
#
# This service is the API-only backend; the user interface lives in the
# companion webui facade (himalsimkhada/webui), which proxies to it.
#
# Offers two deployment modes:
#   1. Full Docker stack   : BIND9 container + API container
#   2. Host BIND + Docker  : manage an existing HOST BIND with the API container
#
# The one-liner does NOT clone the repository: it creates a directory
# (default ~/bind9-webui) and fetches only the compose file (+ shared BIND
# config for mode 1) needed for the mode you pick.
#
# Usage:  sudo ./install.sh   (or: ./install.sh --check | --help)
# One-liner:  curl -fsSL https://raw.githubusercontent.com/himalsimkhada/bind9-webui/main/install.sh | bash

set -euo pipefail

REPO_URL="https://github.com/himalsimkhada/bind9-webui.git"
RAW_BASE="https://raw.githubusercontent.com/himalsimkhada/bind9-webui/main"
WEBUI_REPO_URL="https://github.com/himalsimkhada/webui.git"
WEBUI_PORT="${WEBUI_PORT:-8080}"

DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$DIR"

# ── Output helpers ───────────────────────────────────────────────────────

if [ -t 1 ]; then
  C_RESET=$'\e[0m'; C_GREEN=$'\e[32m'; C_YELLOW=$'\e[33m'; C_RED=$'\e[31m'; C_BOLD=$'\e[1m'
else
  C_RESET=""; C_GREEN=""; C_YELLOW=""; C_RED=""; C_BOLD=""
fi

info()  { printf '%s\n' "${C_BOLD}==>${C_RESET} $*"; }
ok()    { printf '%s%s%s\n' "${C_GREEN}    $*${C_RESET}"; }
warn()  { printf '%s%s%s\n' "${C_YELLOW}!!  $*${C_RESET}"; }
die()   { printf '%s%s%s\n' "${C_RED}FATAL:$*${C_RESET}" >&2; exit 1; }

has_cmd() { command -v "$1" >/dev/null 2>&1; }

# ── Input helpers ────────────────────────────────────────────────────────
# Under `curl ... | bash` stdin is the script stream, so bare `read` never
# sees a prompt. Read from the controlling terminal when one exists. For
# headless runs (no terminal) the variables below can be pre-set in the
# environment instead of prompting.

have_tty() { ( exec </dev/tty ) 2>/dev/null; }

read_input() {
  local var="$1" prompt="${2:-}"
  local envval=""
  case "$var" in
    target) envval="${TARGET_DIR:-}";;
    choice) envval="${BIND9_MODE:-}";;
    ans)    envval="${BIND9_YES:-}";;
  esac
  if [ -n "$envval" ]; then
    printf -v "$var" '%s' "$envval"
    return 0
  fi
  if have_tty; then
    read -r -p "$prompt" "$var" < /dev/tty
    return 0
  fi
  if [ "$var" = "target" ]; then
    printf -v "$var" '%s' "$PWD/bind9-webui"   # headless default: ./bind9-webui subdir
    return 0
  fi
  die "No terminal available and \$$var was not set (${prompt%:}). Re-run from a terminal or set the env var."
}

read_input_silent() {
  local var="$1" prompt="${2:-}"
  if have_tty; then
    read -r -s -p "$prompt" "$var" < /dev/tty
  else
    die "No terminal available for password input. Set WEBUI_PASSWORD=... and re-run."
  fi
  echo ""
}

# ── Streaming bootstrap ──────────────────────────────────────────────────
# Runs when the script is streamed (curl ... | bash) and there is no local
# checkout: ask where to install, create that directory and fetch the files
# for the chosen mode later (ensure_mode_files). No full repo clone.

if [ ! -f "$DIR/docker-compose.yml" ] && [ ! -f "$DIR/docker-compose-w-bind9.yml" ]; then
  echo "==> One-liner install: no project checkout in \"$DIR\"."
  has_cmd curl || has_cmd wget || die "curl or wget is required for the one-liner install."
  has_cmd git || warn "git is not installed — only the files needed by the installer will be fetched."

  default_target="$PWD/bind9-webui"
  target=""
  read_input target "Install the project into [$default_target]: "
  target="${target:-$default_target}"

  mkdir -p "$target"
  cd "$target"
  DIR="$target"
  info "Project dir: $DIR (the compose file is fetched for the mode you choose)"
fi

# ── File fetch (no repo clone) ───────────────────────────────────────────

fetch_file() {
  local rel="$1"
  local dest="$DIR/$rel"
  mkdir -p "$(dirname "$dest")"
  if has_cmd curl; then
    curl -fsSL "$RAW_BASE/$rel" -o "$dest"
  elif has_cmd wget; then
    wget -qO "$dest" "$RAW_BASE/$rel"
  else
    die "curl or wget is required to fetch $rel"
  fi
}

ensure_mode_files() {
  # The compose file is always saved as docker-compose.yml — whatever mode is
  # picked, `docker compose up` picks it up with no -f flag.
  local mode="$1"
  if [ "$mode" = "1" ]; then
    # The full-stack manifest ships in the repo as docker-compose-w-bind9.yml.
    if [ -f "$DIR/docker-compose-w-bind9.yml" ]; then
      cp -f "$DIR/docker-compose-w-bind9.yml" "$DIR/docker-compose.yml"
    elif has_cmd curl; then
      curl -fsSL "$RAW_BASE/docker-compose-w-bind9.yml" -o "$DIR/docker-compose.yml"
    elif has_cmd wget; then
      wget -qO "$DIR/docker-compose.yml" "$RAW_BASE/docker-compose-w-bind9.yml"
    else
      die "curl or wget is required to fetch docker-compose.yml"
    fi
    for f in named.conf named.conf.options named.conf.local named.conf.default-zones rndc.key root.hints db.local db.127 db.0 db.255; do
      [ -f "$DIR/docker/bind/$f" ] || fetch_file "docker/bind/$f"
    done
  else
    [ -f "$DIR/docker-compose.yml" ] || fetch_file "docker-compose.yml"
  fi
  ok "Mode $mode files present in $DIR (compose file: docker-compose.yml)"
}

# ── System detection ─────────────────────────────────────────────────────

OS_ID="unknown"
OS_NAME="unknown"
if [ -r /etc/os-release ]; then
  # shellcheck disable=SC1091
  . /etc/os-release
  OS_ID="${ID:-unknown}"
  OS_NAME="${NAME:-$OS_ID}"
fi

is_deb() { case "$OS_ID" in debian|ubuntu|linuxmint|pop|elementary|kali|raspbian) return 0;; *) return 1;; esac; }
is_rpm() { case "$OS_ID" in rhel|fedora|centos|almalinux|rocky|ol|amazon) return 0;; *) return 1;; esac; }
is_arch() { case "$OS_ID" in arch|manjaro|endeavouros) return 0;; *) return 1;; esac; }

pkg_install() {
  if is_deb; then
    sudo apt-get update -qq
    sudo apt-get install -y -qq "$@"
  elif is_rpm; then
    if command -v dnf >/dev/null 2>&1; then sudo dnf install -y "$@"
    else sudo yum install -y "$@"; fi
  elif is_arch; then
    sudo pacman -S --noconfirm --needed "$@"
  else
    die "Unsupported distro ($OS_NAME). Please install dependencies manually."
  fi
}

# BIND package names / paths per family (overridable via BIND_CONF_DIR env).
bind_packages() {
  if is_deb; then echo "bind9 bind9utils bind9-dnsutils"
  elif is_rpm; then echo "bind bind-utils"
  elif is_arch; then echo "bind"
  fi
}

bind_conf_dir() {
  if [ -n "${BIND_CONF_DIR:-}" ]; then echo "$BIND_CONF_DIR"; return; fi
  if is_deb; then echo "/etc/bind"; else echo "/etc/named"; fi
}

bind_log_dir() {
  if [ -n "${HOST_BIND_LOG_DIR:-}" ]; then echo "$HOST_BIND_LOG_DIR"; return; fi
  if is_deb; then echo "/var/log/bind"; else echo "/var/log/named"; fi
}

rndc_key_path() {
  local dir; dir="$(bind_conf_dir)"
  if [ -n "${RNDC_KEY:-}" ]; then echo "$RNDC_KEY"; return; fi
  if is_deb; then echo "$dir/rndc.key"; else echo "/etc/rndc.key"; fi
}

# ── Generic helpers ──────────────────────────────────────────────────────

random_secret() {
  if has_cmd openssl; then
    openssl rand -hex 32
  else
    od -An -N32 -tx1 /dev/urandom | tr -d ' \n'
  fi
}

ask_password() {
  # Accepts WEBUI_PASSWORD from the environment (headless runs) or prompts.
  if [ -z "${WEBUI_PASSWORD:-}" ]; then
    WEBUI_PASSWORD=""
    while [ -z "$WEBUI_PASSWORD" ]; do
      read_input_silent WEBUI_PASSWORD "    Web UI password (used to log in): "
      if [ -z "$WEBUI_PASSWORD" ]; then
        warn "Password cannot be empty."
      else
        read_input_silent WEBUI_PASSWORD_CONFIRM "    Confirm password: "
        if [ "$WEBUI_PASSWORD" != "$WEBUI_PASSWORD_CONFIRM" ]; then
          warn "Passwords do not match. Try again."
          WEBUI_PASSWORD=""
        fi
      fi
    done
  else
    ok "Using WEBUI_PASSWORD from the environment"
  fi
  SECRET_KEY="$(random_secret)"
}

write_env_file() {
  # Writes .env from an array of KEY=VALUE lines passed on stdin.
  local envpath="$DIR/.env"
  info "Writing $envpath"
  cat > "$envpath"
  chmod 600 "$envpath" 2>/dev/null || true
}

# ── webui admin facade (companion dashboard) ────────────────────────────

webui_detected() {
  # True when the webui admin facade is already answering on the expected port.
  if has_cmd curl && curl -fsS --max-time 3 "http://127.0.0.1:${WEBUI_PORT}/healthz" >/dev/null 2>&1; then
    return 0
  fi
  if has_cmd docker && docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^webui-admin$'; then
    return 0
  fi
  return 1
}

offer_webui_portal() {
  # The dashboard UI lives in the companion webui facade. If it is not running,
  # offer to install it now.
  local tgt="$HOME/webui"
  if webui_detected; then
    ok "webui admin dashboard already running on http://127.0.0.1:${WEBUI_PORT}"
    return
  fi

  if [ -d "$tgt" ] && [ -f "$tgt/install.sh" ]; then
    warn "The webui admin dashboard is NOT running but a checkout exists at $tgt (port $WEBUI_PORT)."
  else
    warn "The webui admin dashboard (himalsimkhada/webui) was not detected on http://127.0.0.1:${WEBUI_PORT}."
  fi
  info "The BIND dashboard is hosted by the webui facade; without it you only get the API."
  ans=""
  read_input ans "    Install the webui admin dashboard now? [Y/n] "
  if [ "${ans:-y}" = "n" ] || [ "${ans:-y}" = "N" ]; then
    warn "Install it later with:  curl -fsSL https://raw.githubusercontent.com/himalsimkhada/webui/main/install.sh | bash"
    return
  fi

  has_cmd git || die "git is required to clone the webui dashboard."
  mkdir -p "$HOME"
  if [ ! -f "$tgt/install.sh" ]; then
    info "Cloning $WEBUI_REPO_URL into $tgt"
    git clone --quiet --depth 1 "$WEBUI_REPO_URL" "$tgt"
  fi
  info "Running the webui dashboard installer (choose Docker or Manual mode)"
  bash "$tgt/install.sh"
  ok "webui admin dashboard installed. Open http://localhost:${WEBUI_PORT}"
}

ensure_named_running() {
  info "Starting named if it is not running"
  sudo systemctl enable named >/dev/null 2>&1 || true
  sudo systemctl start named 2>/dev/null || sudo named 2>/dev/null || true
  sleep 1
  if ! sudo rndc status >/dev/null 2>&1; then
    warn "named may not be running. Check with: sudo rndc status"
  else
    ok "named is running"
  fi
}

ensure_rndc_key() {
  local key; key="$(rndc_key_path)"
  local dir; dir="$(bind_conf_dir)"
  if [ ! -f "$key" ]; then
    info "Generating rndc key at $key"
    sudo rndc-confgen -a
  fi
  if [ -d "$dir" ] && [ -f "$key" ]; then
    sudo chmod 640 "$key" 2>/dev/null || true
    sudo chown root:bind "$key" 2>/dev/null || sudo chown root:named "$key" 2>/dev/null || true
  fi
}

add_logging_channel() {
  # Adds a file channel + default category so the Logs tab has content.
  local dir; dir="$(bind_conf_dir)"
  local conf="$dir/named.conf.options"
  local logfile="$(bind_log_dir)/named.log"
  local marker="# BIND9-WEBUI-LOGGING"
  if [ ! -f "$conf" ]; then
    conf="$dir/named.conf"
  fi
  if ! sudo grep -qF "$marker" "$conf" 2>/dev/null; then
    info "Adding a file logging channel ($logfile) to $conf"
    {
      echo ""
      echo "$marker"
      echo "logging {"
      echo "    channel bind_webui_file { file \"$logfile\" versions 3 size 5m; severity info; };"
      echo "    category default { bind_webui_file; };"
      echo "};"
    } | sudo tee -a "$conf" >/dev/null
    sudo systemctl restart named || true
  else
    ok "Logging channel already present"
  fi
  local ldir; ldir="$(bind_log_dir)"
  sudo mkdir -p "$ldir"
  sudo chown root:bind "$ldir" 2>/dev/null || sudo chown root:named "$ldir" 2>/dev/null || true
}

add_rndc_controls() {
  # Ensures host named listens on TCP 953 for the web-UI container, restricted
  # to loopback + Docker bridge subnets.
  local dir; dir="$(bind_conf_dir)"
  local keyfile; keyfile="$(rndc_key_path)"
  local keyname
  keyname="$(sudo sed -n 's/^key *"\([^"]*\)".*/\1/p' "$keyfile" 2>/dev/null | head -n1)"
  keyname="${keyname:-rndc-key}"
  local marker="# BIND9-WEBUI-CONTROLS"
  local conf="$dir/named.conf"

  if ! grep -q 'port 953' "$conf" 2>/dev/null && ! sudo grep -qF "$marker" "$conf" 2>/dev/null; then
    warn "Host '$dir/named.conf' has no rndc 'controls' block listening on TCP 953,"
    warn "which the web-UI container needs to manage BIND. Adding a restricted one:"
    {
      echo ""
      echo "$marker"
      echo "controls { inet 0.0.0.0 port 953 allow { 127.0.0.1; ::1; 172.16.0.0/12; } keys { \"$keyname\"; }; };"
    } | sudo tee -a "$conf" >/dev/null
    info "Restarting named to apply the controls block"
    sudo systemctl restart named
    ok "rndc TCP 953 control channel enabled (restricted to loopback + 172.16.0.0/12)"
  else
    ok "rndc 953 controls block already present"
  fi
}

ensure_docker() {
  if ! has_cmd docker || ! docker compose version >/dev/null 2>&1; then
    warn "Docker with the compose plugin is required but not installed."
    ans=""
    read_input ans "    Install Docker now? [y/N] "
    if [ "${ans:-n}" != "y" ] && [ "${ans:-n}" != "Y" ]; then
      die "Docker is required for this mode. Re-run after installing Docker."
    fi
    info "Installing Docker"
    if is_deb; then
      sudo apt-get update -qq
      sudo apt-get install -y -qq docker.io docker-compose-v2
    elif is_rpm; then
      sudo dnf install -y moby-engine docker-compose-plugin 2>/dev/null \
        || sudo dnf install -y moby-engine || \
        warn "Could not auto-install Docker. Install it manually and re-run."
    elif is_arch; then
      sudo pacman -S --noconfirm --needed docker docker-compose 2>/dev/null \
        || sudo pacman -S --noconfirm --needed docker
    else
      die "Unsupported distro for automatic Docker install. Install Docker manually."
    fi
  fi

  if ! docker compose version >/dev/null 2>&1; then
    die "Docker compose plugin is missing. Install docker-compose and re-run."
  fi
  if ! docker info >/dev/null 2>&1; then
    warn "Docker daemon is not reachable. Starting the docker service..."
    sudo systemctl enable --now docker >/dev/null 2>&1 || true
    sleep 2
  fi
  docker info >/dev/null 2>&1 \
    || die "Docker daemon is not reachable. Start it (sudo systemctl start docker) and re-run."
  ok "Docker + compose plugin available and the daemon is running"
}

ensure_host_bind() {
  if ! has_cmd named; then
    warn "BIND9 (named) is not installed."
    ans=""
    read_input ans "    Install BIND9 now? [y/N] "
    if [ "${ans:-n}" != "y" ] && [ "${ans:-n}" != "Y" ]; then
      die "BIND9 is required for this mode."
    fi
    # shellcheck disable=SC2046
    pkg_install $(bind_packages)
  fi
  ok "BIND9 present"
  ensure_rndc_key
}

# ── Mode 1: Full Docker stack ────────────────────────────────────────────

seed_bind_volume() {
  # The full-stack compose keeps the whole /etc/bind in one named volume
  # (bind9-vol). The ISC image's own /etc/bind only ships bind.keys +
  # named.conf, so on first run we copy the shipped config files in.
  local vol="bind9-vol"
  if ! docker volume inspect "$vol" >/dev/null 2>&1; then
    info "Initializing $vol from the shipped BIND config (docker/bind)"
    docker run --rm --entrypoint sh \
      -v "$vol":/etc/bind \
      -v "$DIR/docker/bind":/seed:ro \
      internetsystemsconsortium/bind9:9.18 -c '\
        cp -a /seed/. /etc/bind/ && \
        mkdir -p /etc/bind/zones && \
        chown -R 53:53 /etc/bind/zones'
    ok "bind9-vol seeded — edit config with: docker run --rm -it -v bind9-vol:/etc/bind sh"
  else
    ok "bind9-vol already initialized"
  fi
}

mode_docker_full() {
  info "Mode 1: Full Docker stack (BIND9 + API containers)"
  ensure_mode_files 1
  ensure_docker
  seed_bind_volume
  ask_password
  write_env_file <<EOF
WEBUI_PASSWORD=$WEBUI_PASSWORD
SECRET_KEY=$SECRET_KEY
EOF
  info "Starting containers (pulls the API image on first run)"
  # Brings the project down first so the per-project network is recreated
  # fresh: reusing a stale network can crash-loop the BIND container (it dies
  # shortly after start, before named even opens its log).
  docker compose -f docker-compose.yml down >/dev/null 2>&1 || true
  # Containers keep the pinned names across projects; compose can only remove
  # its own, so clear the names when an overlapping install left them behind.
  docker rm -f bind9 bind9-webui >/dev/null 2>&1 || true
  docker compose -f docker-compose.yml up -d
  ok "Deployed. API at http://localhost:5000 (UI lives in the webui facade)"
  ok "DNS is published on host 127.0.0.1:5353 (rndc on 127.0.0.1:9353)"

  offer_webui_portal
}

# ── Mode 2: Host BIND + Docker API ────────────────────────────────────────

mode_host_bind_docker() {
  info "Mode 2: Host BIND9 + API container"
  ensure_mode_files 2
  ensure_docker
  ensure_host_bind
  ensure_named_running
  add_rndc_controls
  add_logging_channel

  local bdir; bdir="$(bind_conf_dir)"
  local ldir; ldir="$(bind_log_dir)"
  ask_password
  write_env_file <<EOF
RNDC_HOST=host.docker.internal
RNDC_PORT=953
BIND_HOST_DIR=$bdir
HOST_BIND_LOG_DIR=$ldir
LOG_FILE=$ldir/named.log
WEBUI_PORT=5000
WEBUI_PASSWORD=$WEBUI_PASSWORD
SECRET_KEY=$SECRET_KEY
EOF

  info "Starting the API container (pulls the image on first run)"
  docker compose -f docker-compose.yml down >/dev/null 2>&1 || true
  docker rm -f bind9-webui >/dev/null 2>&1 || true
  docker compose -f docker-compose.yml up -d
  ok "Deployed. API at http://localhost:5000 (UI lives in the webui facade)"
  ok "Mounted host BIND config from: $bdir"
  ok "Tailing logs from:             $ldir/named.log"

  offer_webui_portal
}

# ── Main menu / flags ────────────────────────────────────────────────────

show_menu() {
  echo ""
  echo "Select how you want to run the BIND9 backend:"
  echo ""
  echo "  1) Full Docker stack   - BIND9 and the API both in containers"
  echo "  2) Host BIND + Docker  - API container managing BIND installed on this machine"
  echo ""
  while :; do
    choice=""
    read_input choice "Enter your choice [1-2]: "
    case "$choice" in
      1) mode_docker_full; return;;
      2) mode_host_bind_docker; return;;
      *) warn "Please choose 1 or 2.";;
    esac
  done
}

do_check() {
  echo "── System check ─────────────────────────────"
  echo "Distro        : $OS_NAME ($OS_ID)"
  echo "Package tool  : $(is_deb && echo 'apt' || (is_rpm && echo 'rpm/dnf' || (is_arch && echo 'pacman' || echo 'unknown')))"
  echo "BIND dir      : $(bind_conf_dir)"
  echo "Log dir       : $(bind_log_dir)"
  echo "rndc key      : $(rndc_key_path)"
  echo ""
  if has_cmd named; then
    echo "BIND9         : installed"
  else
    echo "BIND9         : NOT installed"
  fi
  if has_cmd docker && docker compose version >/dev/null 2>&1; then
    echo "Docker+compose: available"
  else
    echo "Docker+compose: missing"
  fi
  echo ""
}

case "${1:-}" in
  --check|-c) do_check; exit 0;;
  --help|-h)
    sed -n '1,17p' "$0" | sed 's/^# \{0,1\}//'; exit 0;;
  "")
    if [ "$(id -u)" -eq 0 ]; then
      warn "Running as root. Prefer running as a normal sudo user on some distros."
    fi
    show_menu
    echo ""
    echo "Done!"
    ;;
  *) echo "Unknown option: $1 (use --help)"; exit 1;;
esac