diff --git a/docs/superpowers/contracts/linux-config-nextcloud-split-2026-08-16.json b/docs/superpowers/contracts/linux-config-nextcloud-split-2026-08-16.json new file mode 100644 index 00000000..d368770b --- /dev/null +++ b/docs/superpowers/contracts/linux-config-nextcloud-split-2026-08-16.json @@ -0,0 +1,19 @@ +{ + "title": "Split setup_nextcloud_raspberry.sh under the cap, deferring check_and_enable_services.sh", + "objective": "Bring the largest linux_configuration script (1383 lines) under the 250-line cap, and confirm that a script built around one shared accumulator array cannot be split without a suppression.", + "acceptance_criteria": [ + "The entry script and all eight new libs are at most 250 lines.", + "The pre-commit shellcheck hook passes on every touched file, with no new suppression and no disable directive.", + "No lib sources a sibling: the entry script sources all eight directly.", + "The stubbed run reports every moved function as defined.", + "Every unguarded cd exposed by the split is fixed rather than suppressed.", + "No script is executed. This one flashes SD cards, installs packages over SSH and configures MariaDB, PHP and Apache.", + "check_and_enable_services.sh is fully restored to 1337 lines, with the unrelated lib already in that directory left in place." + ], + "out_of_scope": [ + "Executing any flash, install or configure step.", + "check_and_enable_services.sh, whose SERVICE_STATUS associative array is declared in one prospective lib and written by every other.", + "Changing what any script does when run, beyond the cd guards." + ], + "verifier": "meta/scripts/verify_shell_split.sh, plus pre-commit run --files over all touched files, plus check_file_length.sh, plus ls of the shared lib/ directory after the revert." +} diff --git a/docs/superpowers/evidence/linux-config-nextcloud-split-2026-08-16.json b/docs/superpowers/evidence/linux-config-nextcloud-split-2026-08-16.json new file mode 100644 index 00000000..49087ebe --- /dev/null +++ b/docs/superpowers/evidence/linux-config-nextcloud-split-2026-08-16.json @@ -0,0 +1,44 @@ +{ + "intent": "Split the largest linux_configuration script under the cap, and defer a second whose every function writes to one shared array.", + "scope": [ + "linux_configuration/scripts/single_use/features/setup_nextcloud_raspberry.sh (1383 -> 231)", + "lib/nc_image.sh, nc_flash.sh, nc_remote.sh, nc_remote_flash.sh, nc_packages.sh, nc_services.sh, nc_php.sh, nc_discover.sh (new)", + "Deferred and fully restored: periodic_background/check_and_enable_services.sh (1337)", + "Non-goal: no script executed" + ], + "changes": [ + "setup_nextcloud_raspberry.sh split five ways in one pass, then three of those libs were carved again - the first pass left nc_image, nc_remote and nc_services at 272, 292 and 277 lines. Eight libs from one 1383-line script.", + "Fixed three unguarded cd /var/www/nextcloud calls, each immediately before an `occ` command. A failed cd would have run the Nextcloud installer or the cron switch against whatever directory the caller was in. Third script in this effort to have this bug.", + "check_and_enable_services.sh was split four ways and then fully reverted. SERVICE_STATUS is declared once and written by every check_* function, so any lib holding those functions fails SC2034 when linted alone. This is the fourth file deferred for shared state, and the clearest: the script IS an accumulator, so there is no seam anywhere in it." + ], + "verification": [ + { + "command": "meta/scripts/verify_shell_split.sh setup_nextcloud_raspberry.sh ", + "result": "fail then pass", + "evidence": "First run reported SC2164 for the unguarded cd calls; after guarding them, 'libs sourced, functions defined' across all eight libs." + }, + { + "command": "shellcheck on each new lib", + "result": "pass", + "evidence": "Clean, with no disable directive. Each lib is checked in isolation, which is what the pre-commit hook does." + }, + { + "command": "git checkout HEAD -- check_and_enable_services.sh, rm its svc_*.sh libs, ls the lib dir", + "result": "pass", + "evidence": "Restored to 1337 lines and shellcheck -x clean. The directory still contains periodic_browser.sh from an earlier commit - confirmed by listing it rather than assuming the revert removed only what it should." + }, + { + "command": "bash ~/utils/scripts/check_file_length.sh --all", + "result": "pass", + "evidence": "setup_nextcloud_raspberry.sh and all eight libs are under the cap; the largest is 182 lines." + } + ], + "risks": [ + "Not executed: it flashes SD cards, installs packages over SSH to a Raspberry Pi, and configures MariaDB, PHP and Apache. The cd guards change the failure path - previously the installer would have run in the wrong directory, now it returns 1 - which is the intended correction but is a behaviour change.", + "check_and_enable_services.sh stays at 1337 lines, now the largest file in the repo. Splitting it means giving each check_* function a return value instead of writing to SERVICE_STATUS, which is a genuine refactor of the script's whole design." + ], + "rollback": [ + "git revert the commit; setup_nextcloud_raspberry.sh returns to 1383 lines and the eight libs disappear.", + "After rollback, re-run bash -n and shellcheck on the restored script." + ] +} diff --git a/linux_configuration/scripts/single_use/features/lib/nc_discover.sh b/linux_configuration/scripts/single_use/features/lib/nc_discover.sh new file mode 100755 index 00000000..3382c626 --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/nc_discover.sh @@ -0,0 +1,171 @@ +#!/bin/bash +# Raspberry Pi discovery on the local network. +# +# Sourced by setup_nextcloud_raspberry.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +# ============================================================================= +# PHASE ALL-REMOTE: Configure and install Nextcloud via SSH +# ============================================================================= + +discover_raspberry_pi() { + log_info "Auto-discovering Raspberry Pi on local network..." + + ensure_dependencies + + # Get local network info + local my_ip + my_ip=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+(\.\d+){3}' | head -1) + local gateway + gateway=$(ip route | grep default | awk '{print $3}' | head -1) + local network="${gateway%.*}.0/24" + + log_info "Local IP: $my_ip, Network: $network" + log_info "Scanning for Raspberry Pi (hostname: $PI_HOSTNAME)..." + + # First try to find by hostname + local pi_ip="" + + # Try resolving hostname directly + pi_ip=$(getent hosts "$PI_HOSTNAME" 2>/dev/null | awk '{print $1}' | head -1) || true + if [[ -z $pi_ip ]]; then + pi_ip=$(getent hosts "${PI_HOSTNAME}.local" 2>/dev/null | awk '{print $1}' | head -1) || true + fi + + if [[ -n $pi_ip ]]; then + log_success "Found Pi by hostname: $pi_ip" + echo "$pi_ip" + return + fi + + # Ping sweep to wake up hosts + log_info "Hostname resolution failed, scanning network..." + nmap -sn -T4 "$network" &>/dev/null || true + + # Scan for SSH-enabled devices (excluding our IP and known laptop) + local ssh_hosts + ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2>/dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | grep -vw "$REMOTE_LAPTOP_IP" 2>/dev/null | sort -u) || true + + if [[ -z $ssh_hosts ]]; then + die "No new SSH-enabled devices found. Is the Pi connected and booted?" + fi + + log_info "Found SSH-enabled devices: $(echo "$ssh_hosts" | tr '\n' ' ')" + + # Try to connect with our Pi credentials + for ip in $ssh_hosts; do + log_info "Trying $ip with user '$PI_USER'..." + + # Try with password + if sshpass -p "$PI_PASSWORD" ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${PI_USER}@${ip}" "hostname" 2>/dev/null | grep -qi "$PI_HOSTNAME"; then + log_success "Found Raspberry Pi at $ip" + echo "$ip" + return + fi + + # Even if hostname doesn't match, check if it's a fresh Pi responding to our credentials + if sshpass -p "$PI_PASSWORD" ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${PI_USER}@${ip}" "echo ok" 2>/dev/null | grep -q "ok"; then + log_success "Found device responding to Pi credentials at $ip" + echo "$ip" + return + fi + done + + die "Could not find Raspberry Pi on network. Make sure it's connected and has finished booting." +} + +phase_all_remote() { + log_info "=== All-Remote: Configure and Install Nextcloud via SSH ===" + + # Auto-discover Pi IP + local pi_ip + pi_ip=$(discover_raspberry_pi) + + if [[ -z $pi_ip ]]; then + die "Failed to discover Raspberry Pi" + fi + + log_info "Using Raspberry Pi at: $pi_ip" + + # PI_PASSWORD should already be set from config file + if [[ -z $PI_PASSWORD ]]; then + die "PI_PASSWORD not set. Did you run flash-remote first?" + fi + + # Copy this script to Pi + log_info "Copying script to Pi..." + sshpass -p "$PI_PASSWORD" scp -o StrictHostKeyChecking=no "$0" "${PI_USER}@${pi_ip}:/tmp/setup_nextcloud.sh" + + # Run configuration phase + log_info "Running configuration phase on Pi..." + sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ + "echo '$PI_PASSWORD' | sudo -S bash /tmp/setup_nextcloud.sh configure" + + # Run Nextcloud installation phase + log_info "Running Nextcloud installation on Pi..." + + # Auto-generate Nextcloud admin password if not set + auto_generate_nextcloud_password + save_config + + log_success "Nextcloud admin user: $NEXTCLOUD_ADMIN_USER" + log_success "Nextcloud admin password: $NEXTCLOUD_ADMIN_PASSWORD" + + sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ + "echo '$PI_PASSWORD' | sudo -S NEXTCLOUD_ADMIN_PASSWORD='$NEXTCLOUD_ADMIN_PASSWORD' NEXTCLOUD_ADMIN_USER='$NEXTCLOUD_ADMIN_USER' bash /tmp/setup_nextcloud.sh nextcloud" + + log_success "All-Remote phase complete!" + echo + log_info "=== Access Information ===" + log_info "Nextcloud URL: http://$pi_ip/nextcloud" + log_info "Admin user: $NEXTCLOUD_ADMIN_USER" + log_info "Admin password: $NEXTCLOUD_ADMIN_PASSWORD" + log_info "All credentials saved in: $CONFIG_FILE" +} + +# ============================================================================= +# MAIN +# ============================================================================= + +show_help() { + cat <<'EOF' +Nextcloud on Raspberry Pi 5 Setup Script + +Usage: ./setup_nextcloud_raspberry.sh + +Commands: + flash Flash Raspberry Pi OS to SD card (locally) + flash-remote Flash SD card on a remote laptop via SSH + configure Configure Pi for remote access (run on Pi after first boot) + nextcloud Install and configure Nextcloud (run on Pi) + all-remote Run configure + nextcloud via SSH from laptop + help Show this help message + +Environment Variables (optional): + PI_HOSTNAME Hostname for the Pi (default: nextcloud-pi) + PI_USER Username for the Pi (default: pi) + PI_PASSWORD Password for Pi user (prompted if not set) + PI_TIMEZONE Timezone (default: Europe/Warsaw) + NEXTCLOUD_ADMIN_USER Nextcloud admin username (default: admin) + NEXTCLOUD_ADMIN_PASSWORD Nextcloud admin password (prompted if not set) + NEXTCLOUD_DATA_DIR Nextcloud data directory (default: /var/www/nextcloud/data) + SD_CARD_DEVICE SD card device path (detected if not set) + REMOTE_LAPTOP_IP IP address of remote laptop (default: 192.168.1.17) + REMOTE_LAPTOP_USER Username on remote laptop (default: kuhy) + +Examples: + # Flash SD card on a remote laptop in your network + ./setup_nextcloud_raspberry.sh flash-remote + + # Flash SD card locally + sudo ./setup_nextcloud_raspberry.sh flash + + # After Pi boots, SSH in and run: + sudo ./setup_nextcloud_raspberry.sh configure + sudo ./setup_nextcloud_raspberry.sh nextcloud + + # Or run all phases remotely after flash: + sudo ./setup_nextcloud_raspberry.sh all-remote +EOF +} diff --git a/linux_configuration/scripts/single_use/features/lib/nc_flash.sh b/linux_configuration/scripts/single_use/features/lib/nc_flash.sh new file mode 100755 index 00000000..cf30cb21 --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/nc_flash.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# The flash phase, SSH key setup and dependency checks. +# +# Sourced by setup_nextcloud_raspberry.sh; split out to keep nc_image.sh under the +# 250-line cap. Sourced rather than run, so it inherits the caller's +# strict mode and the variables defined above the source line. + +phase_flash() { + check_root + + log_info "=== Phase 1: Flash Raspberry Pi OS to SD Card (Local) ===" + + detect_sd_card + local image_path + image_path=$(download_raspberry_pi_os) + flash_sd_card "$image_path" + configure_headless_boot + + log_success "Phase 1 complete!" + echo + log_info "Next steps:" + log_info "1. Insert the SD card into your Raspberry Pi 5" + log_info "2. Connect the Pi to power and network" + log_info "3. Wait 2-3 minutes for first boot" + log_info "4. Find the Pi's IP address and SSH: ssh ${PI_USER}@" + log_info "5. Copy this script to the Pi and run: sudo ./setup_nextcloud_raspberry.sh configure" +} + +# ============================================================================= +# PHASE 1B: Flash Raspberry Pi OS to SD Card on Remote Laptop +# ============================================================================= + +setup_ssh_key_to_remote() { + local remote_host="$1" + local remote_user="$2" + + # Check if we already have passwordless access + if ssh -o BatchMode=yes -o ConnectTimeout=5 "${remote_user}@${remote_host}" "echo 'SSH key works'" 2>/dev/null; then + log_success "SSH key authentication to ${remote_user}@${remote_host} already configured" + return 0 + fi + + log_info "Setting up SSH key authentication to ${remote_user}@${remote_host}..." + + # Check if SSH key exists, if not create one + if [[ ! -f "$HOME/.ssh/id_ed25519" ]] && [[ ! -f "$HOME/.ssh/id_rsa" ]]; then + log_info "No SSH key found, generating one..." + ssh-keygen -t ed25519 -f "$HOME/.ssh/id_ed25519" -N "" -C "$(whoami)@$(hostname)" + log_success "SSH key generated" + fi + + # Copy SSH key to remote host using sshpass if password provided, otherwise prompt + log_info "Copying SSH key to remote laptop (you will be prompted for password)..." + ssh-copy-id -o StrictHostKeyChecking=accept-new "${remote_user}@${remote_host}" + + # Verify it works + if ssh -o BatchMode=yes -o ConnectTimeout=5 "${remote_user}@${remote_host}" "echo 'SSH key works'" 2>/dev/null; then + log_success "SSH key authentication configured successfully" + return 0 + else + die "Failed to set up SSH key authentication" + fi +} + +ensure_dependencies() { + log_info "Ensuring required tools are installed..." + + local missing_packages=() + + # Check for nmap (fast network scanning) + if ! command -v nmap &>/dev/null; then + missing_packages+=("nmap") + fi + + # Check for sshpass (for initial SSH key setup) + if ! command -v sshpass &>/dev/null; then + missing_packages+=("sshpass") + fi + + if [[ ${#missing_packages[@]} -gt 0 ]]; then + log_info "Installing missing packages: ${missing_packages[*]}" + + # Detect package manager and install + if command -v pacman &>/dev/null; then + sudo pacman -S --noconfirm "${missing_packages[@]}" + elif command -v apt-get &>/dev/null; then + sudo apt-get update && sudo apt-get install -y "${missing_packages[@]}" + elif command -v dnf &>/dev/null; then + sudo dnf install -y "${missing_packages[@]}" + elif command -v yum &>/dev/null; then + sudo yum install -y "${missing_packages[@]}" + else + die "Could not detect package manager. Please install manually: ${missing_packages[*]}" + fi + + log_success "Dependencies installed" + fi +} diff --git a/linux_configuration/scripts/single_use/features/lib/nc_image.sh b/linux_configuration/scripts/single_use/features/lib/nc_image.sh new file mode 100755 index 00000000..b2ffbbc8 --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/nc_image.sh @@ -0,0 +1,180 @@ +#!/bin/bash +# Raspberry Pi OS download and SD-card flashing. +# +# Sourced by setup_nextcloud_raspberry.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +download_raspberry_pi_os() { + local download_dir="/tmp/rpi-image" + local image_url="https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2024-11-19/2024-11-19-raspios-bookworm-arm64-lite.img.xz" + local image_file="$download_dir/raspios.img.xz" + local extracted_image="$download_dir/raspios.img" + local expected_size=459000608 # Size in bytes from content-length + + mkdir -p "$download_dir" + + if [[ -f $extracted_image ]]; then + log_info "Using existing image at $extracted_image" + echo "$extracted_image" + return + fi + + # Check if download exists and is complete + if [[ -f $image_file ]]; then + local actual_size + actual_size=$(stat -c%s "$image_file" 2>/dev/null || stat -f%z "$image_file" 2>/dev/null || echo 0) + if [[ $actual_size -lt $expected_size ]]; then + log_warning "Incomplete download detected ($actual_size < $expected_size bytes), re-downloading..." + rm -f "$image_file" + else + log_info "Image archive already downloaded" + fi + fi + + if [[ ! -f $image_file ]]; then + log_info "Downloading Raspberry Pi OS Lite (64-bit)..." + log_info "This may take a while depending on your internet connection..." + + # Try to use aria2c for faster download, fall back to wget/curl + # Redirect all output to stderr so it doesn't interfere with function return value + if command -v aria2c &>/dev/null; then + aria2c -x 4 -c -d "$download_dir" --out="raspios.img.xz" "$image_url" >&2 + elif command -v wget &>/dev/null; then + wget --continue --show-progress -O "$image_file" "$image_url" >&2 + elif command -v curl &>/dev/null; then + curl -L -C - -o "$image_file" "$image_url" --progress-bar >&2 + else + die "No download tool available. Install wget, curl, or aria2c" + fi + + # Verify download size + local actual_size + actual_size=$(stat -c%s "$image_file" 2>/dev/null || stat -f%z "$image_file" 2>/dev/null || echo 0) + if [[ $actual_size -lt $expected_size ]]; then + die "Download incomplete: got $actual_size bytes, expected $expected_size" + fi + log_success "Download complete: $actual_size bytes" + fi + + log_info "Extracting image..." + xz -dk "$image_file" + + if [[ ! -f $extracted_image ]]; then + die "Failed to extract image" + fi + + echo "$extracted_image" +} + +flash_sd_card() { + local image_path="$1" + + log_warning "This will ERASE ALL DATA on $SD_CARD_DEVICE" + read -r -p "Are you sure you want to continue? (yes/no): " confirm + + if [[ $confirm != "yes" ]]; then + die "Aborted by user" + fi + + # Unmount any mounted partitions + log_info "Unmounting partitions on $SD_CARD_DEVICE..." + for partition in "${SD_CARD_DEVICE}"*; do + if mountpoint -q "$partition" 2>/dev/null || mount | grep -q "$partition"; then + umount "$partition" 2>/dev/null || true + fi + done + + log_info "Flashing image to SD card..." + log_info "This will take several minutes..." + + dd if="$image_path" of="$SD_CARD_DEVICE" bs=4M status=progress conv=fsync + + sync + log_success "Image flashed successfully!" +} + +configure_headless_boot() { + log_info "Configuring headless boot (SSH and WiFi)..." + + # Wait for partitions to be available + sleep 2 + partprobe "$SD_CARD_DEVICE" 2>/dev/null || true + sleep 2 + + # Mount boot partition + local boot_partition + if [[ -b "${SD_CARD_DEVICE}1" ]]; then + boot_partition="${SD_CARD_DEVICE}1" + elif [[ -b "${SD_CARD_DEVICE}p1" ]]; then + boot_partition="${SD_CARD_DEVICE}p1" + else + die "Could not find boot partition" + fi + + local boot_mount="/tmp/rpi-boot" + mkdir -p "$boot_mount" + mount "$boot_partition" "$boot_mount" + + # Enable SSH + touch "$boot_mount/ssh" + log_success "SSH enabled" + + # Configure WiFi (optional) + read -r -p "Do you want to configure WiFi? (y/n): " configure_wifi + if [[ $configure_wifi == "y" ]]; then + read -r -p "WiFi SSID: " wifi_ssid + read -r -s -p "WiFi Password: " wifi_password + echo + + cat >"$boot_mount/wpa_supplicant.conf" <"$boot_mount/userconf.txt" + log_success "User '$PI_USER' configured" + + # Set hostname + local root_partition + if [[ -b "${SD_CARD_DEVICE}2" ]]; then + root_partition="${SD_CARD_DEVICE}2" + elif [[ -b "${SD_CARD_DEVICE}p2" ]]; then + root_partition="${SD_CARD_DEVICE}p2" + fi + + if [[ -n $root_partition ]]; then + local root_mount="/tmp/rpi-root" + mkdir -p "$root_mount" + mount "$root_partition" "$root_mount" + + echo "$PI_HOSTNAME" >"$root_mount/etc/hostname" + sed -i "s/raspberrypi/$PI_HOSTNAME/g" "$root_mount/etc/hosts" + + log_success "Hostname set to '$PI_HOSTNAME'" + + umount "$root_mount" + fi + + umount "$boot_mount" + + log_success "SD card configured for headless boot!" + log_info "Insert the SD card into your Raspberry Pi and power it on." + log_info "Find the Pi's IP address from your router or use: nmap -sn 192.168.1.0/24" +} diff --git a/linux_configuration/scripts/single_use/features/lib/nc_packages.sh b/linux_configuration/scripts/single_use/features/lib/nc_packages.sh new file mode 100755 index 00000000..812f40e7 --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/nc_packages.sh @@ -0,0 +1,182 @@ +#!/bin/bash +# Package installation and the apt lock wait. +# +# Sourced by setup_nextcloud_raspberry.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +# ============================================================================= +# PHASE 2: Configure Pi for Remote Access +# ============================================================================= + +wait_for_apt_lock() { + # Wait for any existing apt/dpkg processes to finish + local max_wait=600 # 10 minutes max + local waited=0 + + while fuser /var/lib/dpkg/lock-frontend /var/lib/apt/lists/lock /var/cache/apt/archives/lock >/dev/null 2>&1; do + if [[ $waited -eq 0 ]]; then + log_info "Waiting for other apt/dpkg processes to finish..." + log_info "Current apt processes:" + pgrep -a 'apt|dpkg' | head -5 >&2 || true + fi + sleep 5 + waited=$((waited + 5)) + if [[ $waited -ge $max_wait ]]; then + die "Timeout waiting for apt lock after ${max_wait}s" + fi + if [[ $((waited % 30)) -eq 0 ]]; then + log_info "Still waiting... (${waited}s elapsed)" + fi + done + + if [[ $waited -gt 0 ]]; then + log_success "Apt lock acquired after ${waited}s" + fi +} + +phase_configure() { + check_root + + log_info "=== Phase 2: Configure Raspberry Pi for Remote Access ===" + + # Wait for any existing apt processes + wait_for_apt_lock + + # Fix any broken packages first + log_info "Fixing any broken packages..." + DEBIAN_FRONTEND=noninteractive dpkg --configure -a --force-confdef --force-confold || true + + # Update system - use non-interactive mode and auto-accept config changes + log_info "Updating system packages..." + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade -y + + # Set timezone + log_info "Setting timezone to $PI_TIMEZONE..." + timedatectl set-timezone "$PI_TIMEZONE" + + # Set locale + log_info "Configuring locale..." + sed -i "s/^# *$PI_LOCALE/$PI_LOCALE/" /etc/locale.gen + locale-gen + update-locale LANG="$PI_LOCALE" + + # Configure SSH for security + log_info "Hardening SSH configuration..." + + # Backup original config + cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup + + # Apply security settings + cat >>/etc/ssh/sshd_config.d/hardening.conf <<'EOF' +# Security hardening +PermitRootLogin no +PasswordAuthentication yes +PubkeyAuthentication yes +X11Forwarding no +MaxAuthTries 3 +ClientAliveInterval 300 +ClientAliveCountMax 2 +EOF + + # Restart SSH + systemctl restart sshd + + # Install useful packages + log_info "Installing useful packages..." + apt-get install -y \ + vim \ + htop \ + curl \ + wget \ + git \ + ufw \ + fail2ban \ + unattended-upgrades + + # Configure firewall + log_info "Configuring firewall..." + ufw default deny incoming + ufw default allow outgoing + ufw allow ssh + ufw allow 80/tcp # HTTP + ufw allow 443/tcp # HTTPS + ufw --force enable + + # Configure fail2ban + log_info "Configuring fail2ban..." + cat >/etc/fail2ban/jail.local <<'EOF' +[DEFAULT] +bantime = 1h +findtime = 10m +maxretry = 5 + +[sshd] +enabled = true +port = ssh +filter = sshd +logpath = /var/log/auth.log +maxretry = 3 +EOF + + systemctl enable fail2ban + systemctl restart fail2ban + + # Enable automatic security updates + log_info "Enabling automatic security updates..." + cat >/etc/apt/apt.conf.d/50unattended-upgrades <<'EOF' +Unattended-Upgrade::Origins-Pattern { + "origin=Debian,codename=${distro_codename},label=Debian-Security"; + "origin=Raspbian,codename=${distro_codename},label=Raspbian"; +}; +Unattended-Upgrade::AutoFixInterruptedDpkg "true"; +Unattended-Upgrade::Remove-Unused-Dependencies "true"; +EOF + + systemctl enable unattended-upgrades + + # Display system info + log_info "System information:" + echo "Hostname: $(hostname)" + echo "IP Address: $(hostname -I | awk '{print $1}')" + echo "Kernel: $(uname -r)" + echo "Architecture: $(uname -m)" + + log_success "Phase 2 complete!" + echo + log_info "Next step: Run 'sudo ./setup_nextcloud_raspberry.sh nextcloud' to install Nextcloud" +} + +# ============================================================================= +# PHASE 3: Install Nextcloud +# ============================================================================= + +install_nextcloud_dependencies() { + log_info "Installing Nextcloud dependencies..." + + apt-get update + apt-get install -y \ + apache2 \ + mariadb-server \ + libapache2-mod-php \ + php \ + php-gd \ + php-mysql \ + php-curl \ + php-mbstring \ + php-intl \ + php-gmp \ + php-bcmath \ + php-xml \ + php-zip \ + php-imagick \ + php-apcu \ + php-redis \ + redis-server \ + unzip \ + certbot \ + python3-certbot-apache + + log_success "Dependencies installed" +} diff --git a/linux_configuration/scripts/single_use/features/lib/nc_php.sh b/linux_configuration/scripts/single_use/features/lib/nc_php.sh new file mode 100755 index 00000000..5e6cb0aa --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/nc_php.sh @@ -0,0 +1,173 @@ +#!/bin/bash +# PHP, Redis and the Nextcloud install itself. +# +# Sourced by setup_nextcloud_raspberry.sh; split out to keep nc_services.sh under the +# 250-line cap. Sourced rather than run, so it inherits the caller's +# strict mode and the variables defined above the source line. + +configure_php() { + log_info "Configuring PHP..." + + # Find PHP version + local php_version + php_version=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;') + local php_ini="/etc/php/${php_version}/apache2/php.ini" + + # Backup original + cp "$php_ini" "${php_ini}.backup" + + # Apply Nextcloud recommended settings + sed -i 's/memory_limit = .*/memory_limit = 512M/' "$php_ini" + sed -i 's/upload_max_filesize = .*/upload_max_filesize = 16G/' "$php_ini" + sed -i 's/post_max_size = .*/post_max_size = 16G/' "$php_ini" + sed -i 's/max_execution_time = .*/max_execution_time = 360/' "$php_ini" + sed -i 's/max_input_time = .*/max_input_time = 360/' "$php_ini" + sed -i 's/;date.timezone.*/date.timezone = Europe\/Warsaw/' "$php_ini" + + # Configure OPcache + cat >>"$php_ini" <<'EOF' + +; Nextcloud OPcache settings +opcache.enable=1 +opcache.interned_strings_buffer=16 +opcache.max_accelerated_files=10000 +opcache.memory_consumption=128 +opcache.save_comments=1 +opcache.revalidate_freq=1 +EOF + + # Configure APCu + echo "apc.enable_cli=1" >>"/etc/php/${php_version}/mods-available/apcu.ini" + + systemctl restart apache2 + + log_success "PHP configured" +} + +configure_redis() { + log_info "Configuring Redis..." + + systemctl enable redis-server + systemctl start redis-server + + log_success "Redis configured" +} + +install_nextcloud() { + log_info "Installing Nextcloud..." + + local db_password + db_password=$(cat /root/.nextcloud_db_password) + + if [[ -z $NEXTCLOUD_ADMIN_PASSWORD ]]; then + prompt_password "Enter Nextcloud admin password" NEXTCLOUD_ADMIN_PASSWORD + fi + + # Create data directory + mkdir -p "$NEXTCLOUD_DATA_DIR" + chown -R www-data:www-data "$NEXTCLOUD_DATA_DIR" + + # Get server IP + local server_ip + server_ip=$(hostname -I | awk '{print $1}') + + # Run Nextcloud installer + cd /var/www/nextcloud || { log_error "Nextcloud is not installed at /var/www/nextcloud"; return 1; } + sudo -u www-data php occ maintenance:install \ + --database "mysql" \ + --database-name "nextcloud" \ + --database-user "nextcloud" \ + --database-pass "$db_password" \ + --admin-user "$NEXTCLOUD_ADMIN_USER" \ + --admin-pass "$NEXTCLOUD_ADMIN_PASSWORD" \ + --data-dir "$NEXTCLOUD_DATA_DIR" + + # Add trusted domain + sudo -u www-data php occ config:system:set trusted_domains 1 --value="$server_ip" + sudo -u www-data php occ config:system:set trusted_domains 2 --value="$PI_HOSTNAME" + sudo -u www-data php occ config:system:set trusted_domains 3 --value="$PI_HOSTNAME.local" + + # Configure Redis caching + sudo -u www-data php occ config:system:set memcache.local --value='\OC\Memcache\APCu' + sudo -u www-data php occ config:system:set memcache.distributed --value='\OC\Memcache\Redis' + sudo -u www-data php occ config:system:set memcache.locking --value='\OC\Memcache\Redis' + sudo -u www-data php occ config:system:set redis host --value='localhost' + sudo -u www-data php occ config:system:set redis port --value='6379' --type=integer + + # Set default phone region + sudo -u www-data php occ config:system:set default_phone_region --value='PL' + + # Enable maintenance window + sudo -u www-data php occ config:system:set maintenance_window_start --value=1 --type=integer + + log_success "Nextcloud installed" +} + +setup_nextcloud_cron() { + log_info "Setting up Nextcloud background jobs..." + + # Add cron job for background tasks + crontab -u www-data -l 2>/dev/null || echo "" | crontab -u www-data - + ( + crontab -u www-data -l 2>/dev/null | grep -v 'nextcloud/cron.php' + echo "*/5 * * * * php -f /var/www/nextcloud/cron.php" + ) | crontab -u www-data - + + # Switch to cron background job mode + cd /var/www/nextcloud || { log_error "Nextcloud is not installed at /var/www/nextcloud"; return 1; } + sudo -u www-data php occ background:cron + + log_success "Cron jobs configured" +} + +verify_nextcloud() { + log_info "Verifying Nextcloud installation..." + + local server_ip + server_ip=$(hostname -I | awk '{print $1}') + + # Check if Nextcloud is responding + if curl -s -o /dev/null -w "%{http_code}" "http://${server_ip}/status.php" | grep -q "200"; then + log_success "Nextcloud is responding!" + else + log_warning "Nextcloud may not be fully ready. Check manually." + fi + + # Run Nextcloud check + cd /var/www/nextcloud || { log_error "Nextcloud is not installed at /var/www/nextcloud"; return 1; } + sudo -u www-data php occ status + + echo + log_success "========================================" + log_success "Nextcloud installation complete!" + log_success "========================================" + echo + log_info "Access Nextcloud at: http://${server_ip}" + log_info "Admin user: $NEXTCLOUD_ADMIN_USER" + log_info "Database password saved at: /root/.nextcloud_db_password" + echo + log_info "Recommended next steps:" + log_info "1. Set up a domain name pointing to your Pi" + log_info "2. Configure SSL with: sudo certbot --apache" + log_info "3. Install Nextcloud apps via the web interface" + log_info "4. Configure external storage if needed" +} + +phase_nextcloud() { + check_root + + log_info "=== Phase 3: Install Nextcloud ===" + + install_nextcloud_dependencies + local db_password + db_password=$(configure_mariadb) + download_nextcloud + configure_apache + configure_php + configure_redis + install_nextcloud + setup_nextcloud_cron + verify_nextcloud + + log_success "Phase 3 complete!" +} diff --git a/linux_configuration/scripts/single_use/features/lib/nc_remote.sh b/linux_configuration/scripts/single_use/features/lib/nc_remote.sh new file mode 100755 index 00000000..46bed5ef --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/nc_remote.sh @@ -0,0 +1,131 @@ +#!/bin/bash +# Remote host discovery and connection setup. +# +# Sourced by setup_nextcloud_raspberry.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +discover_remote_laptop() { + log_info "Auto-discovering remote laptop on local network..." + + # Ensure we have the tools we need + ensure_dependencies + + # Get local IP to exclude ourselves (works on both Linux variants) + local my_ip + my_ip=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+(\.\d+){3}' | head -1) + + # Get gateway + local gateway + gateway=$(ip route | grep default | awk '{print $3}' | head -1) + local network="${gateway%.*}.0/24" + + log_info "Local IP: $my_ip, Gateway: $gateway, Network: $network" + + # Use nmap for fast parallel SSH port scanning + log_info "Scanning network for SSH-enabled devices (using nmap)..." + local ssh_hosts + # First do a ping sweep to wake up hosts, then scan SSH port + nmap -sn -T4 "$network" &>/dev/null || true + # Extract IPs from nmap output - grep for report lines then extract IP + ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2>/dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | sort -u) + + if [[ -z $ssh_hosts ]]; then + die "No SSH-enabled devices found on network" + fi + + local host_count + host_count=$(echo "$ssh_hosts" | wc -l) + log_info "Found $host_count SSH-enabled device(s): $(echo "$ssh_hosts" | tr '\n' ' ')" + + # Common usernames to try (in order of preference) + local common_users=("$REMOTE_LAPTOP_USER" "kuchy" "kuhy" "$(whoami)" "pi" "user" "admin") + # Remove duplicates while preserving order + local users=() + for u in "${common_users[@]}"; do + local is_dup=0 + for existing in "${users[@]}"; do + if [[ $u == "$existing" ]]; then + is_dup=1 + break + fi + done + if [[ $is_dup -eq 0 ]]; then + users+=("$u") + fi + done + + log_info "Will try usernames: ${users[*]}" + + # Find a device with passwordless SSH and SD card + local found_laptop="" + local found_user="" + local idx=0 + + for ip in $ssh_hosts; do + idx=$((idx + 1)) + + # Skip gateway + if [[ $ip == "$gateway" ]]; then + log_info "[$idx/$host_count] Skipping $ip (gateway)" + continue + fi + + log_info "[$idx/$host_count] $ip - Trying SSH key access with common usernames..." + + # Try each username + for try_user in "${users[@]}"; do + if ssh -o BatchMode=yes -o ConnectTimeout=2 -o StrictHostKeyChecking=accept-new "${try_user}@${ip}" "echo ok" 2>/dev/null | grep -q "ok"; then + log_success "[$idx/$host_count] $ip - SSH key access confirmed with user '$try_user'!" + found_user="$try_user" + + # Check if there's a removable device (SD card) + log_info "[$idx/$host_count] $ip - Checking for SD card..." + local has_sd + has_sd=$(ssh -o BatchMode=yes -o ConnectTimeout=2 "${try_user}@${ip}" "lsblk -d -o NAME,RM,TRAN 2>/dev/null | grep -E '1.*(usb|mmc)' | head -1" 2>/dev/null || true) + + if [[ -n $has_sd ]]; then + log_success "[$idx/$host_count] $ip - Found SD card: $has_sd" + found_laptop="$ip" + break 2 # Break out of both loops + else + log_warning "[$idx/$host_count] $ip - No SD card detected, saving as fallback..." + if [[ -z $found_laptop ]]; then + found_laptop="$ip" + fi + fi + break # Found working user, move to next IP if no SD card + fi + done + + if [[ -z $found_user ]]; then + log_info "[$idx/$host_count] $ip - No SSH key access with any common username" + fi + done + + # If no passwordless access found, prompt user for username + if [[ -z $found_laptop ]] || [[ -z $found_user ]]; then + log_warning "No device with passwordless SSH found using common usernames." + + # Pick first available SSH host + found_laptop=$(echo "$ssh_hosts" | grep -vw "$gateway" | head -1) + + if [[ -z $found_laptop ]]; then + die "Could not find any suitable SSH-enabled device" + fi + + log_info "Found SSH host at $found_laptop but need credentials." + read -r -p "Enter username for $found_laptop: " found_user + + if [[ -z $found_user ]]; then + die "No username provided" + fi + fi + + REMOTE_LAPTOP_IP="$found_laptop" + REMOTE_LAPTOP_USER="$found_user" + log_success "Selected remote laptop: ${REMOTE_LAPTOP_USER}@${REMOTE_LAPTOP_IP}" + + # Save to config file for future use + save_config +} diff --git a/linux_configuration/scripts/single_use/features/lib/nc_remote_flash.sh b/linux_configuration/scripts/single_use/features/lib/nc_remote_flash.sh new file mode 100755 index 00000000..87c35c0a --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/nc_remote_flash.sh @@ -0,0 +1,167 @@ +#!/bin/bash +# Flashing from a remote laptop, and executing it there. +# +# Sourced by setup_nextcloud_raspberry.sh; split out to keep nc_remote.sh under the +# 250-line cap. Sourced rather than run, so it inherits the caller's +# strict mode and the variables defined above the source line. + +phase_flash_remote() { + log_info "=== Phase 1B: Flash Raspberry Pi OS to SD Card on Remote Laptop ===" + + # Discover and select remote laptop + discover_remote_laptop + + # Set up SSH key authentication + setup_ssh_key_to_remote "$REMOTE_LAPTOP_IP" "$REMOTE_LAPTOP_USER" + + local remote="${REMOTE_LAPTOP_USER}@${REMOTE_LAPTOP_IP}" + + # Check for SD card on remote laptop + log_info "Checking for SD card on remote laptop..." + echo "Block devices on ${remote}:" + ssh "$remote" "lsblk -d -o NAME,SIZE,TYPE,RM,TRAN,MODEL" || true + echo + + # Auto-detect SD card on remote laptop + log_info "Auto-detecting SD card on remote laptop..." + local sd_device + sd_device=$(ssh "$remote" "lsblk -d -o NAME,RM,TRAN | grep -E '1.*(usb|mmc)' | awk '{print \"/dev/\"\$1}' | head -1" 2>/dev/null || true) + + if [[ -z $sd_device ]]; then + die "No SD card detected on remote laptop. Please insert an SD card and try again." + fi + + # Get size for confirmation + local sd_info + # shellcheck disable=SC2029 # Intentional client-side expansion + sd_info=$(ssh "$remote" "lsblk -d -o NAME,SIZE,MODEL $sd_device 2>/dev/null | tail -1" || true) + + log_success "Auto-detected SD card: $sd_device ($sd_info)" + SD_CARD_DEVICE="$sd_device" + + # Verify device exists on remote + # shellcheck disable=SC2029 # Intentional client-side expansion + if ! ssh "$remote" "[[ -b '$SD_CARD_DEVICE' ]]" 2>/dev/null; then + die "Device $SD_CARD_DEVICE does not exist on remote laptop" + fi + + # Auto-generate Pi password if not set + auto_generate_pi_password + log_success "Pi user '$PI_USER' password: $PI_PASSWORD" + + # Generate encrypted password locally + local encrypted_password + encrypted_password=$(echo "$PI_PASSWORD" | openssl passwd -6 -stdin) + + # Save config now so password is stored + save_config + + # Copy this script to remote laptop + log_info "Copying script to remote laptop..." + scp "$0" "${remote}:/tmp/setup_nextcloud_raspberry.sh" + + # Execute flash on remote laptop + log_info "Executing flash on remote laptop..." + log_warning "This will ERASE ALL DATA on ${SD_CARD_DEVICE} on the remote laptop!" + log_info "Proceeding automatically in 5 seconds... (Ctrl+C to cancel)" + sleep 5 + + # Run the flash process on remote laptop + # We pass the pre-encrypted password to avoid interactive prompts + # Using -tt to force TTY allocation even without local tty + ssh -tt "$remote" "sudo SD_CARD_DEVICE='$SD_CARD_DEVICE' PI_USER='$PI_USER' PI_HOSTNAME='$PI_HOSTNAME' bash /tmp/setup_nextcloud_raspberry.sh flash-remote-execute '$encrypted_password'" + + log_success "Phase 1B complete!" + echo + log_info "Next steps:" + log_info "1. Remove SD card from the laptop and insert into Raspberry Pi 5" + log_info "2. Connect the Pi to power and network" + log_info "3. Wait 2-3 minutes for first boot" + log_info "4. Run: ./setup_nextcloud_raspberry.sh configure (on Pi) or all-remote" +} + +# This is called on the remote laptop by phase_flash_remote +phase_flash_remote_execute() { + check_root + + local encrypted_password="${1:-}" + + log_info "=== Executing Flash on Remote Laptop ===" + + if [[ -z $SD_CARD_DEVICE ]]; then + die "SD_CARD_DEVICE not set" + fi + + # Download and flash + local image_path + image_path=$(download_raspberry_pi_os) + + # Unmount any mounted partitions + log_info "Unmounting partitions on $SD_CARD_DEVICE..." + for partition in "${SD_CARD_DEVICE}"*; do + if mountpoint -q "$partition" 2>/dev/null || mount | grep -q "$partition"; then + umount "$partition" 2>/dev/null || true + fi + done + + log_info "Flashing image to SD card..." + dd if="$image_path" of="$SD_CARD_DEVICE" bs=4M status=progress conv=fsync + sync + log_success "Image flashed successfully!" + + # Configure headless boot + log_info "Configuring headless boot..." + sleep 2 + partprobe "$SD_CARD_DEVICE" 2>/dev/null || true + sleep 2 + + # Mount boot partition + local boot_partition + if [[ -b "${SD_CARD_DEVICE}1" ]]; then + boot_partition="${SD_CARD_DEVICE}1" + elif [[ -b "${SD_CARD_DEVICE}p1" ]]; then + boot_partition="${SD_CARD_DEVICE}p1" + else + die "Could not find boot partition" + fi + + local boot_mount="/tmp/rpi-boot" + mkdir -p "$boot_mount" + mount "$boot_partition" "$boot_mount" + + # Enable SSH + touch "$boot_mount/ssh" + log_success "SSH enabled" + + # Create userconf.txt for first user + if [[ -n $encrypted_password ]]; then + echo "${PI_USER}:${encrypted_password}" >"$boot_mount/userconf.txt" + log_success "User '$PI_USER' configured" + fi + + # Set hostname on root partition + local root_partition + if [[ -b "${SD_CARD_DEVICE}2" ]]; then + root_partition="${SD_CARD_DEVICE}2" + elif [[ -b "${SD_CARD_DEVICE}p2" ]]; then + root_partition="${SD_CARD_DEVICE}p2" + fi + + if [[ -n $root_partition ]]; then + local root_mount="/tmp/rpi-root" + mkdir -p "$root_mount" + mount "$root_partition" "$root_mount" + + echo "$PI_HOSTNAME" >"$root_mount/etc/hostname" + sed -i "s/raspberrypi/$PI_HOSTNAME/g" "$root_mount/etc/hosts" + + log_success "Hostname set to '$PI_HOSTNAME'" + + umount "$root_mount" + fi + + umount "$boot_mount" + sync + + log_success "SD card configured for headless boot!" +} diff --git a/linux_configuration/scripts/single_use/features/lib/nc_services.sh b/linux_configuration/scripts/single_use/features/lib/nc_services.sh new file mode 100755 index 00000000..8aba09d8 --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/nc_services.sh @@ -0,0 +1,110 @@ +#!/bin/bash +# MariaDB, PHP and the Nextcloud cron setup. +# +# Sourced by setup_nextcloud_raspberry.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +configure_mariadb() { + log_info "Configuring MariaDB..." + + # Generate random password for Nextcloud DB user + local db_password + db_password=$(openssl rand -base64 24) + + # Start and enable MariaDB + systemctl start mariadb + systemctl enable mariadb + + # Secure MariaDB installation + mysql -e "DELETE FROM mysql.user WHERE User='';" + mysql -e "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');" + mysql -e "DROP DATABASE IF EXISTS test;" + mysql -e "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';" + mysql -e "FLUSH PRIVILEGES;" + + # Create Nextcloud database and user + mysql -e "CREATE DATABASE IF NOT EXISTS nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" + mysql -e "CREATE USER IF NOT EXISTS 'nextcloud'@'localhost' IDENTIFIED BY '$db_password';" + mysql -e "GRANT ALL PRIVILEGES ON nextcloud.* TO 'nextcloud'@'localhost';" + mysql -e "FLUSH PRIVILEGES;" + + # Save password for later use + echo "$db_password" >/root/.nextcloud_db_password + chmod 600 /root/.nextcloud_db_password + + log_success "MariaDB configured" + echo "$db_password" +} + +download_nextcloud() { + log_info "Downloading Nextcloud..." + + local nc_version="30.0.2" + local nc_url="https://download.nextcloud.com/server/releases/nextcloud-${nc_version}.zip" + local download_dir="/tmp" + local nc_zip="$download_dir/nextcloud.zip" + + if [[ -f $nc_zip ]]; then + log_info "Nextcloud archive already downloaded" + else + wget -O "$nc_zip" "$nc_url" + fi + + # Remove existing installation if present + rm -rf /var/www/nextcloud + + # Extract + unzip -q "$nc_zip" -d /var/www/ + + # Set permissions + chown -R www-data:www-data /var/www/nextcloud + + log_success "Nextcloud downloaded and extracted" +} + +configure_apache() { + log_info "Configuring Apache..." + + # Enable required modules + a2enmod rewrite + a2enmod headers + a2enmod env + a2enmod dir + a2enmod mime + a2enmod ssl + + # Get server IP for configuration + local server_ip + server_ip=$(hostname -I | awk '{print $1}') + + # Create Apache virtual host + cat >/etc/apache2/sites-available/nextcloud.conf < + ServerName $server_ip + DocumentRoot /var/www/nextcloud + + + Require all granted + AllowOverride All + Options FollowSymLinks MultiViews + + + Dav off + + + + ErrorLog \${APACHE_LOG_DIR}/nextcloud_error.log + CustomLog \${APACHE_LOG_DIR}/nextcloud_access.log combined + +EOF + + # Enable site and disable default + a2dissite 000-default.conf + a2ensite nextcloud.conf + + # Restart Apache + systemctl restart apache2 + + log_success "Apache configured" +} diff --git a/linux_configuration/scripts/single_use/features/setup_nextcloud_raspberry.sh b/linux_configuration/scripts/single_use/features/setup_nextcloud_raspberry.sh index d14cc277..6f86844a 100755 --- a/linux_configuration/scripts/single_use/features/setup_nextcloud_raspberry.sh +++ b/linux_configuration/scripts/single_use/features/setup_nextcloud_raspberry.sh @@ -183,1169 +183,23 @@ detect_sd_card() { fi } -download_raspberry_pi_os() { - local download_dir="/tmp/rpi-image" - local image_url="https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2024-11-19/2024-11-19-raspios-bookworm-arm64-lite.img.xz" - local image_file="$download_dir/raspios.img.xz" - local extracted_image="$download_dir/raspios.img" - local expected_size=459000608 # Size in bytes from content-length +# shellcheck source=lib/nc_image.sh +source "$SCRIPT_DIR/lib/nc_image.sh" +# shellcheck source=lib/nc_flash.sh +source "$SCRIPT_DIR/lib/nc_flash.sh" +# shellcheck source=lib/nc_remote.sh +source "$SCRIPT_DIR/lib/nc_remote.sh" +# shellcheck source=lib/nc_remote_flash.sh +source "$SCRIPT_DIR/lib/nc_remote_flash.sh" +# shellcheck source=lib/nc_packages.sh +source "$SCRIPT_DIR/lib/nc_packages.sh" +# shellcheck source=lib/nc_services.sh +source "$SCRIPT_DIR/lib/nc_services.sh" +# shellcheck source=lib/nc_php.sh +source "$SCRIPT_DIR/lib/nc_php.sh" +# shellcheck source=lib/nc_discover.sh +source "$SCRIPT_DIR/lib/nc_discover.sh" - mkdir -p "$download_dir" - - if [[ -f $extracted_image ]]; then - log_info "Using existing image at $extracted_image" - echo "$extracted_image" - return - fi - - # Check if download exists and is complete - if [[ -f $image_file ]]; then - local actual_size - actual_size=$(stat -c%s "$image_file" 2>/dev/null || stat -f%z "$image_file" 2>/dev/null || echo 0) - if [[ $actual_size -lt $expected_size ]]; then - log_warning "Incomplete download detected ($actual_size < $expected_size bytes), re-downloading..." - rm -f "$image_file" - else - log_info "Image archive already downloaded" - fi - fi - - if [[ ! -f $image_file ]]; then - log_info "Downloading Raspberry Pi OS Lite (64-bit)..." - log_info "This may take a while depending on your internet connection..." - - # Try to use aria2c for faster download, fall back to wget/curl - # Redirect all output to stderr so it doesn't interfere with function return value - if command -v aria2c &>/dev/null; then - aria2c -x 4 -c -d "$download_dir" --out="raspios.img.xz" "$image_url" >&2 - elif command -v wget &>/dev/null; then - wget --continue --show-progress -O "$image_file" "$image_url" >&2 - elif command -v curl &>/dev/null; then - curl -L -C - -o "$image_file" "$image_url" --progress-bar >&2 - else - die "No download tool available. Install wget, curl, or aria2c" - fi - - # Verify download size - local actual_size - actual_size=$(stat -c%s "$image_file" 2>/dev/null || stat -f%z "$image_file" 2>/dev/null || echo 0) - if [[ $actual_size -lt $expected_size ]]; then - die "Download incomplete: got $actual_size bytes, expected $expected_size" - fi - log_success "Download complete: $actual_size bytes" - fi - - log_info "Extracting image..." - xz -dk "$image_file" - - if [[ ! -f $extracted_image ]]; then - die "Failed to extract image" - fi - - echo "$extracted_image" -} - -flash_sd_card() { - local image_path="$1" - - log_warning "This will ERASE ALL DATA on $SD_CARD_DEVICE" - read -r -p "Are you sure you want to continue? (yes/no): " confirm - - if [[ $confirm != "yes" ]]; then - die "Aborted by user" - fi - - # Unmount any mounted partitions - log_info "Unmounting partitions on $SD_CARD_DEVICE..." - for partition in "${SD_CARD_DEVICE}"*; do - if mountpoint -q "$partition" 2>/dev/null || mount | grep -q "$partition"; then - umount "$partition" 2>/dev/null || true - fi - done - - log_info "Flashing image to SD card..." - log_info "This will take several minutes..." - - dd if="$image_path" of="$SD_CARD_DEVICE" bs=4M status=progress conv=fsync - - sync - log_success "Image flashed successfully!" -} - -configure_headless_boot() { - log_info "Configuring headless boot (SSH and WiFi)..." - - # Wait for partitions to be available - sleep 2 - partprobe "$SD_CARD_DEVICE" 2>/dev/null || true - sleep 2 - - # Mount boot partition - local boot_partition - if [[ -b "${SD_CARD_DEVICE}1" ]]; then - boot_partition="${SD_CARD_DEVICE}1" - elif [[ -b "${SD_CARD_DEVICE}p1" ]]; then - boot_partition="${SD_CARD_DEVICE}p1" - else - die "Could not find boot partition" - fi - - local boot_mount="/tmp/rpi-boot" - mkdir -p "$boot_mount" - mount "$boot_partition" "$boot_mount" - - # Enable SSH - touch "$boot_mount/ssh" - log_success "SSH enabled" - - # Configure WiFi (optional) - read -r -p "Do you want to configure WiFi? (y/n): " configure_wifi - if [[ $configure_wifi == "y" ]]; then - read -r -p "WiFi SSID: " wifi_ssid - read -r -s -p "WiFi Password: " wifi_password - echo - - cat >"$boot_mount/wpa_supplicant.conf" <"$boot_mount/userconf.txt" - log_success "User '$PI_USER' configured" - - # Set hostname - local root_partition - if [[ -b "${SD_CARD_DEVICE}2" ]]; then - root_partition="${SD_CARD_DEVICE}2" - elif [[ -b "${SD_CARD_DEVICE}p2" ]]; then - root_partition="${SD_CARD_DEVICE}p2" - fi - - if [[ -n $root_partition ]]; then - local root_mount="/tmp/rpi-root" - mkdir -p "$root_mount" - mount "$root_partition" "$root_mount" - - echo "$PI_HOSTNAME" >"$root_mount/etc/hostname" - sed -i "s/raspberrypi/$PI_HOSTNAME/g" "$root_mount/etc/hosts" - - log_success "Hostname set to '$PI_HOSTNAME'" - - umount "$root_mount" - fi - - umount "$boot_mount" - - log_success "SD card configured for headless boot!" - log_info "Insert the SD card into your Raspberry Pi and power it on." - log_info "Find the Pi's IP address from your router or use: nmap -sn 192.168.1.0/24" -} - -phase_flash() { - check_root - - log_info "=== Phase 1: Flash Raspberry Pi OS to SD Card (Local) ===" - - detect_sd_card - local image_path - image_path=$(download_raspberry_pi_os) - flash_sd_card "$image_path" - configure_headless_boot - - log_success "Phase 1 complete!" - echo - log_info "Next steps:" - log_info "1. Insert the SD card into your Raspberry Pi 5" - log_info "2. Connect the Pi to power and network" - log_info "3. Wait 2-3 minutes for first boot" - log_info "4. Find the Pi's IP address and SSH: ssh ${PI_USER}@" - log_info "5. Copy this script to the Pi and run: sudo ./setup_nextcloud_raspberry.sh configure" -} - -# ============================================================================= -# PHASE 1B: Flash Raspberry Pi OS to SD Card on Remote Laptop -# ============================================================================= - -setup_ssh_key_to_remote() { - local remote_host="$1" - local remote_user="$2" - - # Check if we already have passwordless access - if ssh -o BatchMode=yes -o ConnectTimeout=5 "${remote_user}@${remote_host}" "echo 'SSH key works'" 2>/dev/null; then - log_success "SSH key authentication to ${remote_user}@${remote_host} already configured" - return 0 - fi - - log_info "Setting up SSH key authentication to ${remote_user}@${remote_host}..." - - # Check if SSH key exists, if not create one - if [[ ! -f "$HOME/.ssh/id_ed25519" ]] && [[ ! -f "$HOME/.ssh/id_rsa" ]]; then - log_info "No SSH key found, generating one..." - ssh-keygen -t ed25519 -f "$HOME/.ssh/id_ed25519" -N "" -C "$(whoami)@$(hostname)" - log_success "SSH key generated" - fi - - # Copy SSH key to remote host using sshpass if password provided, otherwise prompt - log_info "Copying SSH key to remote laptop (you will be prompted for password)..." - ssh-copy-id -o StrictHostKeyChecking=accept-new "${remote_user}@${remote_host}" - - # Verify it works - if ssh -o BatchMode=yes -o ConnectTimeout=5 "${remote_user}@${remote_host}" "echo 'SSH key works'" 2>/dev/null; then - log_success "SSH key authentication configured successfully" - return 0 - else - die "Failed to set up SSH key authentication" - fi -} - -ensure_dependencies() { - log_info "Ensuring required tools are installed..." - - local missing_packages=() - - # Check for nmap (fast network scanning) - if ! command -v nmap &>/dev/null; then - missing_packages+=("nmap") - fi - - # Check for sshpass (for initial SSH key setup) - if ! command -v sshpass &>/dev/null; then - missing_packages+=("sshpass") - fi - - if [[ ${#missing_packages[@]} -gt 0 ]]; then - log_info "Installing missing packages: ${missing_packages[*]}" - - # Detect package manager and install - if command -v pacman &>/dev/null; then - sudo pacman -S --noconfirm "${missing_packages[@]}" - elif command -v apt-get &>/dev/null; then - sudo apt-get update && sudo apt-get install -y "${missing_packages[@]}" - elif command -v dnf &>/dev/null; then - sudo dnf install -y "${missing_packages[@]}" - elif command -v yum &>/dev/null; then - sudo yum install -y "${missing_packages[@]}" - else - die "Could not detect package manager. Please install manually: ${missing_packages[*]}" - fi - - log_success "Dependencies installed" - fi -} - -discover_remote_laptop() { - log_info "Auto-discovering remote laptop on local network..." - - # Ensure we have the tools we need - ensure_dependencies - - # Get local IP to exclude ourselves (works on both Linux variants) - local my_ip - my_ip=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+(\.\d+){3}' | head -1) - - # Get gateway - local gateway - gateway=$(ip route | grep default | awk '{print $3}' | head -1) - local network="${gateway%.*}.0/24" - - log_info "Local IP: $my_ip, Gateway: $gateway, Network: $network" - - # Use nmap for fast parallel SSH port scanning - log_info "Scanning network for SSH-enabled devices (using nmap)..." - local ssh_hosts - # First do a ping sweep to wake up hosts, then scan SSH port - nmap -sn -T4 "$network" &>/dev/null || true - # Extract IPs from nmap output - grep for report lines then extract IP - ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2>/dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | sort -u) - - if [[ -z $ssh_hosts ]]; then - die "No SSH-enabled devices found on network" - fi - - local host_count - host_count=$(echo "$ssh_hosts" | wc -l) - log_info "Found $host_count SSH-enabled device(s): $(echo "$ssh_hosts" | tr '\n' ' ')" - - # Common usernames to try (in order of preference) - local common_users=("$REMOTE_LAPTOP_USER" "kuchy" "kuhy" "$(whoami)" "pi" "user" "admin") - # Remove duplicates while preserving order - local users=() - for u in "${common_users[@]}"; do - local is_dup=0 - for existing in "${users[@]}"; do - if [[ $u == "$existing" ]]; then - is_dup=1 - break - fi - done - if [[ $is_dup -eq 0 ]]; then - users+=("$u") - fi - done - - log_info "Will try usernames: ${users[*]}" - - # Find a device with passwordless SSH and SD card - local found_laptop="" - local found_user="" - local idx=0 - - for ip in $ssh_hosts; do - idx=$((idx + 1)) - - # Skip gateway - if [[ $ip == "$gateway" ]]; then - log_info "[$idx/$host_count] Skipping $ip (gateway)" - continue - fi - - log_info "[$idx/$host_count] $ip - Trying SSH key access with common usernames..." - - # Try each username - for try_user in "${users[@]}"; do - if ssh -o BatchMode=yes -o ConnectTimeout=2 -o StrictHostKeyChecking=accept-new "${try_user}@${ip}" "echo ok" 2>/dev/null | grep -q "ok"; then - log_success "[$idx/$host_count] $ip - SSH key access confirmed with user '$try_user'!" - found_user="$try_user" - - # Check if there's a removable device (SD card) - log_info "[$idx/$host_count] $ip - Checking for SD card..." - local has_sd - has_sd=$(ssh -o BatchMode=yes -o ConnectTimeout=2 "${try_user}@${ip}" "lsblk -d -o NAME,RM,TRAN 2>/dev/null | grep -E '1.*(usb|mmc)' | head -1" 2>/dev/null || true) - - if [[ -n $has_sd ]]; then - log_success "[$idx/$host_count] $ip - Found SD card: $has_sd" - found_laptop="$ip" - break 2 # Break out of both loops - else - log_warning "[$idx/$host_count] $ip - No SD card detected, saving as fallback..." - if [[ -z $found_laptop ]]; then - found_laptop="$ip" - fi - fi - break # Found working user, move to next IP if no SD card - fi - done - - if [[ -z $found_user ]]; then - log_info "[$idx/$host_count] $ip - No SSH key access with any common username" - fi - done - - # If no passwordless access found, prompt user for username - if [[ -z $found_laptop ]] || [[ -z $found_user ]]; then - log_warning "No device with passwordless SSH found using common usernames." - - # Pick first available SSH host - found_laptop=$(echo "$ssh_hosts" | grep -vw "$gateway" | head -1) - - if [[ -z $found_laptop ]]; then - die "Could not find any suitable SSH-enabled device" - fi - - log_info "Found SSH host at $found_laptop but need credentials." - read -r -p "Enter username for $found_laptop: " found_user - - if [[ -z $found_user ]]; then - die "No username provided" - fi - fi - - REMOTE_LAPTOP_IP="$found_laptop" - REMOTE_LAPTOP_USER="$found_user" - log_success "Selected remote laptop: ${REMOTE_LAPTOP_USER}@${REMOTE_LAPTOP_IP}" - - # Save to config file for future use - save_config -} - -phase_flash_remote() { - log_info "=== Phase 1B: Flash Raspberry Pi OS to SD Card on Remote Laptop ===" - - # Discover and select remote laptop - discover_remote_laptop - - # Set up SSH key authentication - setup_ssh_key_to_remote "$REMOTE_LAPTOP_IP" "$REMOTE_LAPTOP_USER" - - local remote="${REMOTE_LAPTOP_USER}@${REMOTE_LAPTOP_IP}" - - # Check for SD card on remote laptop - log_info "Checking for SD card on remote laptop..." - echo "Block devices on ${remote}:" - ssh "$remote" "lsblk -d -o NAME,SIZE,TYPE,RM,TRAN,MODEL" || true - echo - - # Auto-detect SD card on remote laptop - log_info "Auto-detecting SD card on remote laptop..." - local sd_device - sd_device=$(ssh "$remote" "lsblk -d -o NAME,RM,TRAN | grep -E '1.*(usb|mmc)' | awk '{print \"/dev/\"\$1}' | head -1" 2>/dev/null || true) - - if [[ -z $sd_device ]]; then - die "No SD card detected on remote laptop. Please insert an SD card and try again." - fi - - # Get size for confirmation - local sd_info - # shellcheck disable=SC2029 # Intentional client-side expansion - sd_info=$(ssh "$remote" "lsblk -d -o NAME,SIZE,MODEL $sd_device 2>/dev/null | tail -1" || true) - - log_success "Auto-detected SD card: $sd_device ($sd_info)" - SD_CARD_DEVICE="$sd_device" - - # Verify device exists on remote - # shellcheck disable=SC2029 # Intentional client-side expansion - if ! ssh "$remote" "[[ -b '$SD_CARD_DEVICE' ]]" 2>/dev/null; then - die "Device $SD_CARD_DEVICE does not exist on remote laptop" - fi - - # Auto-generate Pi password if not set - auto_generate_pi_password - log_success "Pi user '$PI_USER' password: $PI_PASSWORD" - - # Generate encrypted password locally - local encrypted_password - encrypted_password=$(echo "$PI_PASSWORD" | openssl passwd -6 -stdin) - - # Save config now so password is stored - save_config - - # Copy this script to remote laptop - log_info "Copying script to remote laptop..." - scp "$0" "${remote}:/tmp/setup_nextcloud_raspberry.sh" - - # Execute flash on remote laptop - log_info "Executing flash on remote laptop..." - log_warning "This will ERASE ALL DATA on ${SD_CARD_DEVICE} on the remote laptop!" - log_info "Proceeding automatically in 5 seconds... (Ctrl+C to cancel)" - sleep 5 - - # Run the flash process on remote laptop - # We pass the pre-encrypted password to avoid interactive prompts - # Using -tt to force TTY allocation even without local tty - ssh -tt "$remote" "sudo SD_CARD_DEVICE='$SD_CARD_DEVICE' PI_USER='$PI_USER' PI_HOSTNAME='$PI_HOSTNAME' bash /tmp/setup_nextcloud_raspberry.sh flash-remote-execute '$encrypted_password'" - - log_success "Phase 1B complete!" - echo - log_info "Next steps:" - log_info "1. Remove SD card from the laptop and insert into Raspberry Pi 5" - log_info "2. Connect the Pi to power and network" - log_info "3. Wait 2-3 minutes for first boot" - log_info "4. Run: ./setup_nextcloud_raspberry.sh configure (on Pi) or all-remote" -} - -# This is called on the remote laptop by phase_flash_remote -phase_flash_remote_execute() { - check_root - - local encrypted_password="${1:-}" - - log_info "=== Executing Flash on Remote Laptop ===" - - if [[ -z $SD_CARD_DEVICE ]]; then - die "SD_CARD_DEVICE not set" - fi - - # Download and flash - local image_path - image_path=$(download_raspberry_pi_os) - - # Unmount any mounted partitions - log_info "Unmounting partitions on $SD_CARD_DEVICE..." - for partition in "${SD_CARD_DEVICE}"*; do - if mountpoint -q "$partition" 2>/dev/null || mount | grep -q "$partition"; then - umount "$partition" 2>/dev/null || true - fi - done - - log_info "Flashing image to SD card..." - dd if="$image_path" of="$SD_CARD_DEVICE" bs=4M status=progress conv=fsync - sync - log_success "Image flashed successfully!" - - # Configure headless boot - log_info "Configuring headless boot..." - sleep 2 - partprobe "$SD_CARD_DEVICE" 2>/dev/null || true - sleep 2 - - # Mount boot partition - local boot_partition - if [[ -b "${SD_CARD_DEVICE}1" ]]; then - boot_partition="${SD_CARD_DEVICE}1" - elif [[ -b "${SD_CARD_DEVICE}p1" ]]; then - boot_partition="${SD_CARD_DEVICE}p1" - else - die "Could not find boot partition" - fi - - local boot_mount="/tmp/rpi-boot" - mkdir -p "$boot_mount" - mount "$boot_partition" "$boot_mount" - - # Enable SSH - touch "$boot_mount/ssh" - log_success "SSH enabled" - - # Create userconf.txt for first user - if [[ -n $encrypted_password ]]; then - echo "${PI_USER}:${encrypted_password}" >"$boot_mount/userconf.txt" - log_success "User '$PI_USER' configured" - fi - - # Set hostname on root partition - local root_partition - if [[ -b "${SD_CARD_DEVICE}2" ]]; then - root_partition="${SD_CARD_DEVICE}2" - elif [[ -b "${SD_CARD_DEVICE}p2" ]]; then - root_partition="${SD_CARD_DEVICE}p2" - fi - - if [[ -n $root_partition ]]; then - local root_mount="/tmp/rpi-root" - mkdir -p "$root_mount" - mount "$root_partition" "$root_mount" - - echo "$PI_HOSTNAME" >"$root_mount/etc/hostname" - sed -i "s/raspberrypi/$PI_HOSTNAME/g" "$root_mount/etc/hosts" - - log_success "Hostname set to '$PI_HOSTNAME'" - - umount "$root_mount" - fi - - umount "$boot_mount" - sync - - log_success "SD card configured for headless boot!" -} - -# ============================================================================= -# PHASE 2: Configure Pi for Remote Access -# ============================================================================= - -wait_for_apt_lock() { - # Wait for any existing apt/dpkg processes to finish - local max_wait=600 # 10 minutes max - local waited=0 - - while fuser /var/lib/dpkg/lock-frontend /var/lib/apt/lists/lock /var/cache/apt/archives/lock >/dev/null 2>&1; do - if [[ $waited -eq 0 ]]; then - log_info "Waiting for other apt/dpkg processes to finish..." - log_info "Current apt processes:" - pgrep -a 'apt|dpkg' | head -5 >&2 || true - fi - sleep 5 - waited=$((waited + 5)) - if [[ $waited -ge $max_wait ]]; then - die "Timeout waiting for apt lock after ${max_wait}s" - fi - if [[ $((waited % 30)) -eq 0 ]]; then - log_info "Still waiting... (${waited}s elapsed)" - fi - done - - if [[ $waited -gt 0 ]]; then - log_success "Apt lock acquired after ${waited}s" - fi -} - -phase_configure() { - check_root - - log_info "=== Phase 2: Configure Raspberry Pi for Remote Access ===" - - # Wait for any existing apt processes - wait_for_apt_lock - - # Fix any broken packages first - log_info "Fixing any broken packages..." - DEBIAN_FRONTEND=noninteractive dpkg --configure -a --force-confdef --force-confold || true - - # Update system - use non-interactive mode and auto-accept config changes - log_info "Updating system packages..." - apt-get update - DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade -y - - # Set timezone - log_info "Setting timezone to $PI_TIMEZONE..." - timedatectl set-timezone "$PI_TIMEZONE" - - # Set locale - log_info "Configuring locale..." - sed -i "s/^# *$PI_LOCALE/$PI_LOCALE/" /etc/locale.gen - locale-gen - update-locale LANG="$PI_LOCALE" - - # Configure SSH for security - log_info "Hardening SSH configuration..." - - # Backup original config - cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup - - # Apply security settings - cat >>/etc/ssh/sshd_config.d/hardening.conf <<'EOF' -# Security hardening -PermitRootLogin no -PasswordAuthentication yes -PubkeyAuthentication yes -X11Forwarding no -MaxAuthTries 3 -ClientAliveInterval 300 -ClientAliveCountMax 2 -EOF - - # Restart SSH - systemctl restart sshd - - # Install useful packages - log_info "Installing useful packages..." - apt-get install -y \ - vim \ - htop \ - curl \ - wget \ - git \ - ufw \ - fail2ban \ - unattended-upgrades - - # Configure firewall - log_info "Configuring firewall..." - ufw default deny incoming - ufw default allow outgoing - ufw allow ssh - ufw allow 80/tcp # HTTP - ufw allow 443/tcp # HTTPS - ufw --force enable - - # Configure fail2ban - log_info "Configuring fail2ban..." - cat >/etc/fail2ban/jail.local <<'EOF' -[DEFAULT] -bantime = 1h -findtime = 10m -maxretry = 5 - -[sshd] -enabled = true -port = ssh -filter = sshd -logpath = /var/log/auth.log -maxretry = 3 -EOF - - systemctl enable fail2ban - systemctl restart fail2ban - - # Enable automatic security updates - log_info "Enabling automatic security updates..." - cat >/etc/apt/apt.conf.d/50unattended-upgrades <<'EOF' -Unattended-Upgrade::Origins-Pattern { - "origin=Debian,codename=${distro_codename},label=Debian-Security"; - "origin=Raspbian,codename=${distro_codename},label=Raspbian"; -}; -Unattended-Upgrade::AutoFixInterruptedDpkg "true"; -Unattended-Upgrade::Remove-Unused-Dependencies "true"; -EOF - - systemctl enable unattended-upgrades - - # Display system info - log_info "System information:" - echo "Hostname: $(hostname)" - echo "IP Address: $(hostname -I | awk '{print $1}')" - echo "Kernel: $(uname -r)" - echo "Architecture: $(uname -m)" - - log_success "Phase 2 complete!" - echo - log_info "Next step: Run 'sudo ./setup_nextcloud_raspberry.sh nextcloud' to install Nextcloud" -} - -# ============================================================================= -# PHASE 3: Install Nextcloud -# ============================================================================= - -install_nextcloud_dependencies() { - log_info "Installing Nextcloud dependencies..." - - apt-get update - apt-get install -y \ - apache2 \ - mariadb-server \ - libapache2-mod-php \ - php \ - php-gd \ - php-mysql \ - php-curl \ - php-mbstring \ - php-intl \ - php-gmp \ - php-bcmath \ - php-xml \ - php-zip \ - php-imagick \ - php-apcu \ - php-redis \ - redis-server \ - unzip \ - certbot \ - python3-certbot-apache - - log_success "Dependencies installed" -} - -configure_mariadb() { - log_info "Configuring MariaDB..." - - # Generate random password for Nextcloud DB user - local db_password - db_password=$(openssl rand -base64 24) - - # Start and enable MariaDB - systemctl start mariadb - systemctl enable mariadb - - # Secure MariaDB installation - mysql -e "DELETE FROM mysql.user WHERE User='';" - mysql -e "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');" - mysql -e "DROP DATABASE IF EXISTS test;" - mysql -e "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';" - mysql -e "FLUSH PRIVILEGES;" - - # Create Nextcloud database and user - mysql -e "CREATE DATABASE IF NOT EXISTS nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" - mysql -e "CREATE USER IF NOT EXISTS 'nextcloud'@'localhost' IDENTIFIED BY '$db_password';" - mysql -e "GRANT ALL PRIVILEGES ON nextcloud.* TO 'nextcloud'@'localhost';" - mysql -e "FLUSH PRIVILEGES;" - - # Save password for later use - echo "$db_password" >/root/.nextcloud_db_password - chmod 600 /root/.nextcloud_db_password - - log_success "MariaDB configured" - echo "$db_password" -} - -download_nextcloud() { - log_info "Downloading Nextcloud..." - - local nc_version="30.0.2" - local nc_url="https://download.nextcloud.com/server/releases/nextcloud-${nc_version}.zip" - local download_dir="/tmp" - local nc_zip="$download_dir/nextcloud.zip" - - if [[ -f $nc_zip ]]; then - log_info "Nextcloud archive already downloaded" - else - wget -O "$nc_zip" "$nc_url" - fi - - # Remove existing installation if present - rm -rf /var/www/nextcloud - - # Extract - unzip -q "$nc_zip" -d /var/www/ - - # Set permissions - chown -R www-data:www-data /var/www/nextcloud - - log_success "Nextcloud downloaded and extracted" -} - -configure_apache() { - log_info "Configuring Apache..." - - # Enable required modules - a2enmod rewrite - a2enmod headers - a2enmod env - a2enmod dir - a2enmod mime - a2enmod ssl - - # Get server IP for configuration - local server_ip - server_ip=$(hostname -I | awk '{print $1}') - - # Create Apache virtual host - cat >/etc/apache2/sites-available/nextcloud.conf < - ServerName $server_ip - DocumentRoot /var/www/nextcloud - - - Require all granted - AllowOverride All - Options FollowSymLinks MultiViews - - - Dav off - - - - ErrorLog \${APACHE_LOG_DIR}/nextcloud_error.log - CustomLog \${APACHE_LOG_DIR}/nextcloud_access.log combined - -EOF - - # Enable site and disable default - a2dissite 000-default.conf - a2ensite nextcloud.conf - - # Restart Apache - systemctl restart apache2 - - log_success "Apache configured" -} - -configure_php() { - log_info "Configuring PHP..." - - # Find PHP version - local php_version - php_version=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;') - local php_ini="/etc/php/${php_version}/apache2/php.ini" - - # Backup original - cp "$php_ini" "${php_ini}.backup" - - # Apply Nextcloud recommended settings - sed -i 's/memory_limit = .*/memory_limit = 512M/' "$php_ini" - sed -i 's/upload_max_filesize = .*/upload_max_filesize = 16G/' "$php_ini" - sed -i 's/post_max_size = .*/post_max_size = 16G/' "$php_ini" - sed -i 's/max_execution_time = .*/max_execution_time = 360/' "$php_ini" - sed -i 's/max_input_time = .*/max_input_time = 360/' "$php_ini" - sed -i 's/;date.timezone.*/date.timezone = Europe\/Warsaw/' "$php_ini" - - # Configure OPcache - cat >>"$php_ini" <<'EOF' - -; Nextcloud OPcache settings -opcache.enable=1 -opcache.interned_strings_buffer=16 -opcache.max_accelerated_files=10000 -opcache.memory_consumption=128 -opcache.save_comments=1 -opcache.revalidate_freq=1 -EOF - - # Configure APCu - echo "apc.enable_cli=1" >>"/etc/php/${php_version}/mods-available/apcu.ini" - - systemctl restart apache2 - - log_success "PHP configured" -} - -configure_redis() { - log_info "Configuring Redis..." - - systemctl enable redis-server - systemctl start redis-server - - log_success "Redis configured" -} - -install_nextcloud() { - log_info "Installing Nextcloud..." - - local db_password - db_password=$(cat /root/.nextcloud_db_password) - - if [[ -z $NEXTCLOUD_ADMIN_PASSWORD ]]; then - prompt_password "Enter Nextcloud admin password" NEXTCLOUD_ADMIN_PASSWORD - fi - - # Create data directory - mkdir -p "$NEXTCLOUD_DATA_DIR" - chown -R www-data:www-data "$NEXTCLOUD_DATA_DIR" - - # Get server IP - local server_ip - server_ip=$(hostname -I | awk '{print $1}') - - # Run Nextcloud installer - cd /var/www/nextcloud - sudo -u www-data php occ maintenance:install \ - --database "mysql" \ - --database-name "nextcloud" \ - --database-user "nextcloud" \ - --database-pass "$db_password" \ - --admin-user "$NEXTCLOUD_ADMIN_USER" \ - --admin-pass "$NEXTCLOUD_ADMIN_PASSWORD" \ - --data-dir "$NEXTCLOUD_DATA_DIR" - - # Add trusted domain - sudo -u www-data php occ config:system:set trusted_domains 1 --value="$server_ip" - sudo -u www-data php occ config:system:set trusted_domains 2 --value="$PI_HOSTNAME" - sudo -u www-data php occ config:system:set trusted_domains 3 --value="$PI_HOSTNAME.local" - - # Configure Redis caching - sudo -u www-data php occ config:system:set memcache.local --value='\OC\Memcache\APCu' - sudo -u www-data php occ config:system:set memcache.distributed --value='\OC\Memcache\Redis' - sudo -u www-data php occ config:system:set memcache.locking --value='\OC\Memcache\Redis' - sudo -u www-data php occ config:system:set redis host --value='localhost' - sudo -u www-data php occ config:system:set redis port --value='6379' --type=integer - - # Set default phone region - sudo -u www-data php occ config:system:set default_phone_region --value='PL' - - # Enable maintenance window - sudo -u www-data php occ config:system:set maintenance_window_start --value=1 --type=integer - - log_success "Nextcloud installed" -} - -setup_nextcloud_cron() { - log_info "Setting up Nextcloud background jobs..." - - # Add cron job for background tasks - crontab -u www-data -l 2>/dev/null || echo "" | crontab -u www-data - - ( - crontab -u www-data -l 2>/dev/null | grep -v 'nextcloud/cron.php' - echo "*/5 * * * * php -f /var/www/nextcloud/cron.php" - ) | crontab -u www-data - - - # Switch to cron background job mode - cd /var/www/nextcloud - sudo -u www-data php occ background:cron - - log_success "Cron jobs configured" -} - -verify_nextcloud() { - log_info "Verifying Nextcloud installation..." - - local server_ip - server_ip=$(hostname -I | awk '{print $1}') - - # Check if Nextcloud is responding - if curl -s -o /dev/null -w "%{http_code}" "http://${server_ip}/status.php" | grep -q "200"; then - log_success "Nextcloud is responding!" - else - log_warning "Nextcloud may not be fully ready. Check manually." - fi - - # Run Nextcloud check - cd /var/www/nextcloud - sudo -u www-data php occ status - - echo - log_success "========================================" - log_success "Nextcloud installation complete!" - log_success "========================================" - echo - log_info "Access Nextcloud at: http://${server_ip}" - log_info "Admin user: $NEXTCLOUD_ADMIN_USER" - log_info "Database password saved at: /root/.nextcloud_db_password" - echo - log_info "Recommended next steps:" - log_info "1. Set up a domain name pointing to your Pi" - log_info "2. Configure SSL with: sudo certbot --apache" - log_info "3. Install Nextcloud apps via the web interface" - log_info "4. Configure external storage if needed" -} - -phase_nextcloud() { - check_root - - log_info "=== Phase 3: Install Nextcloud ===" - - install_nextcloud_dependencies - local db_password - db_password=$(configure_mariadb) - download_nextcloud - configure_apache - configure_php - configure_redis - install_nextcloud - setup_nextcloud_cron - verify_nextcloud - - log_success "Phase 3 complete!" -} - -# ============================================================================= -# PHASE ALL-REMOTE: Configure and install Nextcloud via SSH -# ============================================================================= - -discover_raspberry_pi() { - log_info "Auto-discovering Raspberry Pi on local network..." - - ensure_dependencies - - # Get local network info - local my_ip - my_ip=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+(\.\d+){3}' | head -1) - local gateway - gateway=$(ip route | grep default | awk '{print $3}' | head -1) - local network="${gateway%.*}.0/24" - - log_info "Local IP: $my_ip, Network: $network" - log_info "Scanning for Raspberry Pi (hostname: $PI_HOSTNAME)..." - - # First try to find by hostname - local pi_ip="" - - # Try resolving hostname directly - pi_ip=$(getent hosts "$PI_HOSTNAME" 2>/dev/null | awk '{print $1}' | head -1) || true - if [[ -z $pi_ip ]]; then - pi_ip=$(getent hosts "${PI_HOSTNAME}.local" 2>/dev/null | awk '{print $1}' | head -1) || true - fi - - if [[ -n $pi_ip ]]; then - log_success "Found Pi by hostname: $pi_ip" - echo "$pi_ip" - return - fi - - # Ping sweep to wake up hosts - log_info "Hostname resolution failed, scanning network..." - nmap -sn -T4 "$network" &>/dev/null || true - - # Scan for SSH-enabled devices (excluding our IP and known laptop) - local ssh_hosts - ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2>/dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | grep -vw "$REMOTE_LAPTOP_IP" 2>/dev/null | sort -u) || true - - if [[ -z $ssh_hosts ]]; then - die "No new SSH-enabled devices found. Is the Pi connected and booted?" - fi - - log_info "Found SSH-enabled devices: $(echo "$ssh_hosts" | tr '\n' ' ')" - - # Try to connect with our Pi credentials - for ip in $ssh_hosts; do - log_info "Trying $ip with user '$PI_USER'..." - - # Try with password - if sshpass -p "$PI_PASSWORD" ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${PI_USER}@${ip}" "hostname" 2>/dev/null | grep -qi "$PI_HOSTNAME"; then - log_success "Found Raspberry Pi at $ip" - echo "$ip" - return - fi - - # Even if hostname doesn't match, check if it's a fresh Pi responding to our credentials - if sshpass -p "$PI_PASSWORD" ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${PI_USER}@${ip}" "echo ok" 2>/dev/null | grep -q "ok"; then - log_success "Found device responding to Pi credentials at $ip" - echo "$ip" - return - fi - done - - die "Could not find Raspberry Pi on network. Make sure it's connected and has finished booting." -} - -phase_all_remote() { - log_info "=== All-Remote: Configure and Install Nextcloud via SSH ===" - - # Auto-discover Pi IP - local pi_ip - pi_ip=$(discover_raspberry_pi) - - if [[ -z $pi_ip ]]; then - die "Failed to discover Raspberry Pi" - fi - - log_info "Using Raspberry Pi at: $pi_ip" - - # PI_PASSWORD should already be set from config file - if [[ -z $PI_PASSWORD ]]; then - die "PI_PASSWORD not set. Did you run flash-remote first?" - fi - - # Copy this script to Pi - log_info "Copying script to Pi..." - sshpass -p "$PI_PASSWORD" scp -o StrictHostKeyChecking=no "$0" "${PI_USER}@${pi_ip}:/tmp/setup_nextcloud.sh" - - # Run configuration phase - log_info "Running configuration phase on Pi..." - sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ - "echo '$PI_PASSWORD' | sudo -S bash /tmp/setup_nextcloud.sh configure" - - # Run Nextcloud installation phase - log_info "Running Nextcloud installation on Pi..." - - # Auto-generate Nextcloud admin password if not set - auto_generate_nextcloud_password - save_config - - log_success "Nextcloud admin user: $NEXTCLOUD_ADMIN_USER" - log_success "Nextcloud admin password: $NEXTCLOUD_ADMIN_PASSWORD" - - sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ - "echo '$PI_PASSWORD' | sudo -S NEXTCLOUD_ADMIN_PASSWORD='$NEXTCLOUD_ADMIN_PASSWORD' NEXTCLOUD_ADMIN_USER='$NEXTCLOUD_ADMIN_USER' bash /tmp/setup_nextcloud.sh nextcloud" - - log_success "All-Remote phase complete!" - echo - log_info "=== Access Information ===" - log_info "Nextcloud URL: http://$pi_ip/nextcloud" - log_info "Admin user: $NEXTCLOUD_ADMIN_USER" - log_info "Admin password: $NEXTCLOUD_ADMIN_PASSWORD" - log_info "All credentials saved in: $CONFIG_FILE" -} - -# ============================================================================= -# MAIN -# ============================================================================= - -show_help() { - cat <<'EOF' -Nextcloud on Raspberry Pi 5 Setup Script - -Usage: ./setup_nextcloud_raspberry.sh - -Commands: - flash Flash Raspberry Pi OS to SD card (locally) - flash-remote Flash SD card on a remote laptop via SSH - configure Configure Pi for remote access (run on Pi after first boot) - nextcloud Install and configure Nextcloud (run on Pi) - all-remote Run configure + nextcloud via SSH from laptop - help Show this help message - -Environment Variables (optional): - PI_HOSTNAME Hostname for the Pi (default: nextcloud-pi) - PI_USER Username for the Pi (default: pi) - PI_PASSWORD Password for Pi user (prompted if not set) - PI_TIMEZONE Timezone (default: Europe/Warsaw) - NEXTCLOUD_ADMIN_USER Nextcloud admin username (default: admin) - NEXTCLOUD_ADMIN_PASSWORD Nextcloud admin password (prompted if not set) - NEXTCLOUD_DATA_DIR Nextcloud data directory (default: /var/www/nextcloud/data) - SD_CARD_DEVICE SD card device path (detected if not set) - REMOTE_LAPTOP_IP IP address of remote laptop (default: 192.168.1.17) - REMOTE_LAPTOP_USER Username on remote laptop (default: kuhy) - -Examples: - # Flash SD card on a remote laptop in your network - ./setup_nextcloud_raspberry.sh flash-remote - - # Flash SD card locally - sudo ./setup_nextcloud_raspberry.sh flash - - # After Pi boots, SSH in and run: - sudo ./setup_nextcloud_raspberry.sh configure - sudo ./setup_nextcloud_raspberry.sh nextcloud - - # Or run all phases remotely after flash: - sudo ./setup_nextcloud_raspberry.sh all-remote -EOF -} main() { local command="${1:-help}"