358 lines
8.8 KiB
Bash
358 lines
8.8 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# Arch Linux installer for:
|
|
# - R + RStudio Server (via AUR rstudio-server-bin)
|
|
# - JupyterHub + JupyterLab (native install, not TLJH)
|
|
#
|
|
# What it does:
|
|
# - installs system dependencies
|
|
# - installs and enables OpenSSH
|
|
# - installs and safely configures UFW
|
|
# - creates an admin UNIX account
|
|
# - imports the required GPG key for openssl-1.1
|
|
# - installs AUR dependencies for RStudio Server
|
|
# - installs R and RStudio Server
|
|
# - installs JupyterHub into /opt/jupyterhub
|
|
# - configures JupyterHub as a systemd service
|
|
#
|
|
# Run as root:
|
|
# sudo bash install_jhub_rstudio_arch.sh
|
|
#
|
|
|
|
set -Eeuo pipefail
|
|
|
|
# -----------------------------
|
|
# Configuration: EDIT THESE
|
|
# -----------------------------
|
|
ADMIN_USER="your_admin_user"
|
|
ADMIN_PASSWORD="your_admin_password"
|
|
|
|
JUPYTERHUB_PORT="8000"
|
|
RSTUDIO_PORT="8787"
|
|
SSH_PORT="22"
|
|
|
|
JUPYTERHUB_VENV="/opt/jupyterhub"
|
|
JUPYTERHUB_CONFIG_DIR="/etc/jupyterhub"
|
|
JUPYTERHUB_CONFIG_FILE="${JUPYTERHUB_CONFIG_DIR}/jupyterhub_config.py"
|
|
JUPYTERHUB_SERVICE_FILE="/etc/systemd/system/jupyterhub.service"
|
|
|
|
USE_UFW="no"
|
|
|
|
# Required for openssl-1.1 AUR signature verification
|
|
OPENSSL11_GPG_KEY="D894E2CE8B3D79F5"
|
|
OPENSSL11_GPG_KEYSERVER_PRIMARY="hkps://keyserver.ubuntu.com"
|
|
OPENSSL11_GPG_KEYSERVER_FALLBACK="hkps://keys.openpgp.org"
|
|
# -----------------------------
|
|
|
|
log() {
|
|
printf "\n\033[1;36m==> %s\033[0m\n" "$*"
|
|
}
|
|
|
|
warn() {
|
|
printf "\n\033[1;33m[WARN]\033[0m %s\n" "$*"
|
|
}
|
|
|
|
err() {
|
|
printf "\n\033[1;31m[ERROR]\033[0m %s\n" "$*" >&2
|
|
}
|
|
|
|
require_root() {
|
|
if [[ "${EUID}" -ne 0 ]]; then
|
|
err "Run this script as root (sudo)."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
detect_real_user() {
|
|
if [[ -n "${SUDO_USER:-}" && "${SUDO_USER}" != "root" ]]; then
|
|
REAL_USER="${SUDO_USER}"
|
|
else
|
|
REAL_USER="$(logname 2>/dev/null || true)"
|
|
if [[ -z "${REAL_USER}" || "${REAL_USER}" == "root" ]]; then
|
|
err "Could not determine the non-root user for building AUR packages."
|
|
err "Run this with: sudo bash $0 from your normal user session."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
REAL_HOME="$(getent passwd "${REAL_USER}" | cut -d: -f6)"
|
|
if [[ -z "${REAL_HOME}" || ! -d "${REAL_HOME}" ]]; then
|
|
err "Could not determine home directory for ${REAL_USER}."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
install_base_packages() {
|
|
log "Updating pacman and installing base packages"
|
|
|
|
pacman -Syu --noconfirm
|
|
|
|
pacman -S --needed --noconfirm \
|
|
base-devel \
|
|
git \
|
|
curl \
|
|
wget \
|
|
sudo \
|
|
ufw \
|
|
openssh \
|
|
nginx \
|
|
python \
|
|
python-pip \
|
|
python-virtualenv \
|
|
nodejs \
|
|
npm \
|
|
sqlite \
|
|
pam \
|
|
shadow \
|
|
which \
|
|
iproute2 \
|
|
gnupg \
|
|
r \
|
|
gcc-fortran \
|
|
openssl
|
|
}
|
|
|
|
enable_ssh() {
|
|
log "Enabling SSH"
|
|
systemctl enable --now sshd
|
|
}
|
|
|
|
configure_firewall() {
|
|
if [[ "${USE_UFW}" != "yes" ]]; then
|
|
warn "Skipping UFW configuration"
|
|
return
|
|
fi
|
|
|
|
log "Configuring UFW firewall safely"
|
|
|
|
ufw allow "${SSH_PORT}"/tcp
|
|
ufw allow 80/tcp
|
|
ufw allow 443/tcp
|
|
ufw allow "${JUPYTERHUB_PORT}"/tcp
|
|
ufw allow "${RSTUDIO_PORT}"/tcp
|
|
|
|
ufw --force enable
|
|
systemctl enable --now ufw
|
|
}
|
|
|
|
create_admin_user() {
|
|
log "Creating admin system user if needed"
|
|
|
|
if ! id "${ADMIN_USER}" >/dev/null 2>&1; then
|
|
useradd -m -s /bin/bash "${ADMIN_USER}"
|
|
echo "${ADMIN_USER}:${ADMIN_PASSWORD}" | chpasswd
|
|
usermod -aG wheel "${ADMIN_USER}"
|
|
log "Created user ${ADMIN_USER}"
|
|
else
|
|
warn "User ${ADMIN_USER} already exists; updating password"
|
|
echo "${ADMIN_USER}:${ADMIN_PASSWORD}" | chpasswd
|
|
usermod -aG wheel "${ADMIN_USER}" || true
|
|
fi
|
|
}
|
|
|
|
ensure_gpg_key() {
|
|
local key_id="$1"
|
|
|
|
log "Ensuring GPG key is present: ${key_id}"
|
|
|
|
sudo -u "${REAL_USER}" bash -lc "
|
|
set -Eeuo pipefail
|
|
|
|
if gpg --list-keys '${key_id}' >/dev/null 2>&1; then
|
|
exit 0
|
|
fi
|
|
|
|
gpg --keyserver '${OPENSSL11_GPG_KEYSERVER_PRIMARY}' --recv-keys '${key_id}' || \
|
|
gpg --keyserver '${OPENSSL11_GPG_KEYSERVER_FALLBACK}' --recv-keys '${key_id}'
|
|
"
|
|
}
|
|
|
|
install_aur_package() {
|
|
local pkg_name="$1"
|
|
local build_dir="${REAL_HOME}/${pkg_name}-build"
|
|
|
|
log "Installing AUR package: ${pkg_name}"
|
|
|
|
rm -rf "${build_dir}"
|
|
|
|
sudo -u "${REAL_USER}" bash -lc "
|
|
set -Eeuo pipefail
|
|
cd '${REAL_HOME}'
|
|
git clone 'https://aur.archlinux.org/${pkg_name}.git' '${build_dir}'
|
|
cd '${build_dir}'
|
|
makepkg -si --noconfirm --needed
|
|
"
|
|
|
|
rm -rf "${build_dir}"
|
|
}
|
|
|
|
install_rstudio_server() {
|
|
log "Installing RStudio Server from AUR"
|
|
|
|
ensure_gpg_key "${OPENSSL11_GPG_KEY}"
|
|
install_aur_package "openssl-1.1"
|
|
install_aur_package "rstudio-server-bin"
|
|
|
|
mkdir -p /etc/rstudio
|
|
|
|
if ! grep -q '^rsession-which-r=' /etc/rstudio/rserver.conf 2>/dev/null; then
|
|
echo 'rsession-which-r=/usr/bin/R' >> /etc/rstudio/rserver.conf
|
|
else
|
|
sed -i 's|^rsession-which-r=.*|rsession-which-r=/usr/bin/R|' /etc/rstudio/rserver.conf
|
|
fi
|
|
|
|
sed -i '/^server-user=/d' /etc/rstudio/rserver.conf 2>/dev/null || true
|
|
|
|
mkdir -p /var/lib/rstudio-server
|
|
chown -R root:root /var/lib/rstudio-server
|
|
|
|
if [[ ! -e /etc/pam.d/rstudio ]]; then
|
|
cat >/etc/pam.d/rstudio <<'EOF'
|
|
auth include system-login
|
|
account include system-login
|
|
password include system-login
|
|
session include system-login
|
|
EOF
|
|
fi
|
|
|
|
systemctl enable --now rstudio-server
|
|
}
|
|
|
|
install_jupyterhub() {
|
|
log "Installing JupyterHub in virtual environment"
|
|
|
|
mkdir -p "${JUPYTERHUB_VENV}"
|
|
python -m venv "${JUPYTERHUB_VENV}"
|
|
|
|
"${JUPYTERHUB_VENV}/bin/pip" install --upgrade pip wheel setuptools
|
|
|
|
"${JUPYTERHUB_VENV}/bin/pip" install \
|
|
jupyterhub \
|
|
jupyterlab \
|
|
notebook
|
|
|
|
npm install -g configurable-http-proxy
|
|
}
|
|
|
|
configure_jupyterhub() {
|
|
log "Configuring JupyterHub"
|
|
|
|
mkdir -p "${JUPYTERHUB_CONFIG_DIR}"
|
|
|
|
cat >"${JUPYTERHUB_CONFIG_FILE}" <<EOF
|
|
c = get_config()
|
|
|
|
c.JupyterHub.bind_url = 'http://0.0.0.0:${JUPYTERHUB_PORT}'
|
|
c.JupyterHub.hub_ip = '127.0.0.1'
|
|
c.JupyterHub.spawner_class = 'jupyterhub.spawner.LocalProcessSpawner'
|
|
|
|
c.Authenticator.admin_users = {'${ADMIN_USER}'}
|
|
c.JupyterHub.admin_access = True
|
|
|
|
c.Spawner.default_url = '/lab'
|
|
|
|
# Use system users + PAM authentication
|
|
c.JupyterHub.authenticator_class = 'jupyterhub.auth.PAMAuthenticator'
|
|
|
|
# Keep runtime state in standard locations
|
|
c.JupyterHub.cookie_secret_file = '${JUPYTERHUB_CONFIG_DIR}/jupyterhub_cookie_secret'
|
|
c.JupyterHub.db_url = 'sqlite:///${JUPYTERHUB_CONFIG_DIR}/jupyterhub.sqlite'
|
|
c.ConfigurableHTTPProxy.command = 'configurable-http-proxy'
|
|
EOF
|
|
|
|
python - <<'PY'
|
|
import os
|
|
import secrets
|
|
|
|
path = '/etc/jupyterhub/jupyterhub_cookie_secret'
|
|
os.makedirs('/etc/jupyterhub', exist_ok=True)
|
|
|
|
if not os.path.exists(path) or os.path.getsize(path) == 0:
|
|
with open(path, 'w', encoding='utf-8') as f:
|
|
f.write(secrets.token_hex(32) + '\n')
|
|
PY
|
|
|
|
chmod 600 "${JUPYTERHUB_CONFIG_DIR}/jupyterhub_cookie_secret"
|
|
chown root:root "${JUPYTERHUB_CONFIG_DIR}/jupyterhub_cookie_secret"
|
|
|
|
cat >"${JUPYTERHUB_SERVICE_FILE}" <<EOF
|
|
[Unit]
|
|
Description=JupyterHub
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
Environment="PATH=${JUPYTERHUB_VENV}/bin:/usr/local/bin:/usr/bin:/bin"
|
|
ExecStart=${JUPYTERHUB_VENV}/bin/jupyterhub -f ${JUPYTERHUB_CONFIG_FILE}
|
|
WorkingDirectory=${JUPYTERHUB_CONFIG_DIR}
|
|
Restart=always
|
|
RestartSec=10
|
|
User=root
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
systemctl daemon-reload
|
|
systemctl enable --now jupyterhub
|
|
}
|
|
|
|
verify_services() {
|
|
log "Verifying services"
|
|
|
|
systemctl --no-pager --full status sshd || true
|
|
systemctl --no-pager --full status rstudio-server || true
|
|
systemctl --no-pager --full status jupyterhub || true
|
|
systemctl --no-pager --full status ufw || true
|
|
|
|
ss -tulpn | grep -E ":(${SSH_PORT}|${JUPYTERHUB_PORT}|${RSTUDIO_PORT}|80|443)\b" || true
|
|
}
|
|
|
|
print_summary() {
|
|
local server_ip
|
|
server_ip="$(ip -4 addr show scope global | awk '/inet / {print $2}' | cut -d/ -f1 | head -n1)"
|
|
|
|
echo
|
|
echo "=========================================================="
|
|
echo "✅ Installation Summary"
|
|
echo "=========================================================="
|
|
echo "SSH:"
|
|
echo " ssh ${ADMIN_USER}@${server_ip}"
|
|
echo
|
|
echo "JupyterHub:"
|
|
echo " http://${server_ip}:${JUPYTERHUB_PORT}/"
|
|
echo
|
|
echo "RStudio Server:"
|
|
echo " http://${server_ip}:${RSTUDIO_PORT}/"
|
|
echo
|
|
echo "Admin login:"
|
|
echo " ${ADMIN_USER}"
|
|
echo
|
|
echo "Multi-user notes:"
|
|
echo " - JupyterHub uses PAM/local Linux users."
|
|
echo " - RStudio Server uses the same local Linux users."
|
|
echo " - To add more users later:"
|
|
echo " sudo useradd -m -s /bin/bash username"
|
|
echo " sudo passwd username"
|
|
echo
|
|
echo "Important notes:"
|
|
echo " - JupyterHub on this script is a native Arch install, not TLJH."
|
|
echo " - RStudio Server is installed from the AUR package rstudio-server-bin."
|
|
echo "=========================================================="
|
|
}
|
|
|
|
main() {
|
|
require_root
|
|
detect_real_user
|
|
install_base_packages
|
|
enable_ssh
|
|
configure_firewall
|
|
create_admin_user
|
|
install_rstudio_server
|
|
install_jupyterhub
|
|
configure_jupyterhub
|
|
verify_services
|
|
print_summary
|
|
}
|
|
|
|
main "$@" |