From 7181a6dacd433f6edff08af07533062dc444eaa8 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 6 Mar 2026 23:52:01 +0100 Subject: [PATCH] Initial commit: networktuning.sh, gitlab-upgrade.sh, CI pipeline Amp-Thread-ID: https://ampcode.com/threads/T-019cc404-c628-759e-a50b-f5eeea35b91f Co-authored-by: Amp --- .gitlab-ci.yml | 95 ++++++ gitlab-upgrade.sh | 760 ++++++++++++++++++++++++++++++++++++++++++++++ networktuning.sh | 398 ++++++++++++++++++++++++ 3 files changed, 1253 insertions(+) create mode 100644 .gitlab-ci.yml create mode 100644 gitlab-upgrade.sh create mode 100644 networktuning.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..86b8ccd --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,95 @@ +############################################################################### +# .gitlab-ci.yml — CI pipeline for bash script testing +# +# Stages: +# 1. lint — ShellCheck static analysis + bash syntax check +# 2. test — Run --help and --dry-run in Ubuntu and RHEL containers +############################################################################### + +stages: + - lint + - test + +variables: + SHELLCHECK_SEVERITY: "warning" + +# ───────────────────────────────────────────── +# Lint Stage +# ───────────────────────────────────────────── + +shellcheck: + stage: lint + image: koalaman/shellcheck-alpine:stable + script: + - echo "Running ShellCheck on all .sh files..." + - find . -name "*.sh" -not -path "./.git/*" -print0 | + xargs -0 -r shellcheck --severity="$SHELLCHECK_SEVERITY" --format=tty + tags: + - docker + +bash-syntax: + stage: lint + image: bash:5 + script: + - echo "Checking bash syntax (bash -n)..." + - | + errors=0 + for script in $(find . -name "*.sh" -not -path "./.git/*"); do + if ! bash -n "$script" 2>&1; then + errors=$((errors + 1)) + fi + done + if [ "$errors" -gt 0 ]; then + echo "FAILED: $errors script(s) have syntax errors" + exit 1 + fi + echo "All scripts pass syntax check" + tags: + - docker + +# ───────────────────────────────────────────── +# Test Stage — Ubuntu +# ───────────────────────────────────────────── + +test-ubuntu: + stage: test + image: ubuntu:24.04 + before_script: + - apt-get update -qq + - apt-get install -y -qq procps iproute2 kmod >/dev/null 2>&1 + script: + - echo "=== Testing on Ubuntu 24.04 ===" + - | + for script in $(find . -maxdepth 1 -name "*.sh" -not -path "./.git/*"); do + echo "" + echo "--- $(basename "$script") --help ---" + bash "$script" --help 2>&1 || true + done + - echo "" + - echo "--- networktuning.sh --dry-run ---" + - bash networktuning.sh --dry-run 2>&1 || true + tags: + - docker + +# ───────────────────────────────────────────── +# Test Stage — RHEL +# ───────────────────────────────────────────── + +test-rhel: + stage: test + image: rockylinux:9 + before_script: + - dnf install -y -q procps iproute kmod >/dev/null 2>&1 + script: + - echo "=== Testing on Rocky Linux 9 ===" + - | + for script in $(find . -maxdepth 1 -name "*.sh" -not -path "./.git/*"); do + echo "" + echo "--- $(basename "$script") --help ---" + bash "$script" --help 2>&1 || true + done + - echo "" + - echo "--- networktuning.sh --dry-run ---" + - bash networktuning.sh --dry-run 2>&1 || true + tags: + - docker diff --git a/gitlab-upgrade.sh b/gitlab-upgrade.sh new file mode 100644 index 0000000..e0ac0ff --- /dev/null +++ b/gitlab-upgrade.sh @@ -0,0 +1,760 @@ +#!/bin/bash + +################################################ +#### GitLab Upgrade Automation #### +#### Multi-stop upgrade with air-gapped #### +#### environment support #### +#### #### +#### Author: Phil Connor #### +#### Contact: contact@mylinux.work #### +#### Version: 1.00-030526 #### +################################################ + +set -o pipefail + +SCRIPT_NAME=$(basename "$0") +readonly SCRIPT_NAME + +# Required version stops (as of 2026) +readonly VERSION_STOPS=( + "14.0.12" + "14.3.6" + "14.9.5" + "14.10.5" + "15.0.5" + "15.4.6" + "15.11.13" + "16.0.9" + "16.3.8" + "16.7.9" + "16.11.10" + "17.0.8" + "17.3.7" + "17.5.5" + "17.8.7" + "18.0.1" +) + +# Default configuration +readonly DEFAULT_PACKAGE_DIR="/var/opt/gitlab/upgrade-packages" +readonly DEFAULT_BACKUP_DIR="/var/opt/gitlab/backups" +readonly DEFAULT_MIN_DISK_GB=5 +readonly DEFAULT_MIGRATION_TIMEOUT=3600 +readonly DEFAULT_MIGRATION_POLL_INTERVAL=30 + +# Configuration variables (can be overridden by environment) +PACKAGE_DIR=${PACKAGE_DIR:-$DEFAULT_PACKAGE_DIR} +BACKUP_DIR=${BACKUP_DIR:-$DEFAULT_BACKUP_DIR} +MIN_DISK_GB=${MIN_DISK_GB:-$DEFAULT_MIN_DISK_GB} +MIGRATION_TIMEOUT=${MIGRATION_TIMEOUT:-$DEFAULT_MIGRATION_TIMEOUT} +MIGRATION_POLL_INTERVAL=${MIGRATION_POLL_INTERVAL:-$DEFAULT_MIGRATION_POLL_INTERVAL} +DEBUG=${DEBUG:-} + +# Runtime flags +EDITION="" +MODE="upgrade" +AUTO_YES=false +SKIP_BACKUP=false +TARGET_VERSION="" +PKG_MANAGER="" +OS_FAMILY="" +CURRENT_VERSION="" + +handle_error() { + local exit_code=$1 + local line_number=$2 + echo "Error: $SCRIPT_NAME failed at line $line_number with exit code $exit_code" >&2 + exit "$exit_code" +} + +trap 'handle_error $? $LINENO' ERR + +debug_echo() { + if [[ -n "$DEBUG" ]]; then + echo "[DEBUG] $*" >&2 + fi +} + +log_info() { + echo "[INFO] $*" +} + +log_warn() { + echo "[WARN] $*" >&2 +} + +log_error() { + echo "[ERROR] $*" >&2 +} + +show_help() { + cat << EOF +Usage: $SCRIPT_NAME [OPTIONS] + +Automate multi-stop GitLab upgrades with air-gapped environment support. + +Handles required version stops from 14.x through 18.x, background migration +checks, backups, health verification, and supports both online and offline +(air-gapped) installations. + +MODES: + (default) Perform the upgrade (online or offline) + --download-only Download all packages for the upgrade path to PACKAGE_DIR + --list-stops Show the upgrade path for your current version and exit + +OPTIONS: + --target VERSION Target GitLab version (default: latest in stop list) + --edition ce|ee GitLab edition (default: auto-detect from installed package) + --offline Install from local packages in PACKAGE_DIR instead of repos + --yes Skip confirmation prompts between stops + --skip-backup Skip the pre-upgrade backup (not recommended) + --help, -h Show this help message + +ENVIRONMENT VARIABLES: + PACKAGE_DIR Directory for downloaded/offline packages (default: $DEFAULT_PACKAGE_DIR) + BACKUP_DIR Backup directory (default: $DEFAULT_BACKUP_DIR) + MIN_DISK_GB Minimum free disk space in GB (default: $DEFAULT_MIN_DISK_GB) + MIGRATION_TIMEOUT Max seconds to wait for background migrations (default: $DEFAULT_MIGRATION_TIMEOUT) + MIGRATION_POLL_INTERVAL Seconds between migration status checks (default: $DEFAULT_MIGRATION_POLL_INTERVAL) + DEBUG Enable debug output + +EXAMPLES: + # Show what stops are needed + $SCRIPT_NAME --list-stops + + # Upgrade to latest (online) + sudo $SCRIPT_NAME --yes + + # Upgrade to a specific version + sudo $SCRIPT_NAME --target 17.5.5 + + # Download packages for air-gapped transfer + sudo $SCRIPT_NAME --download-only --target 17.5.5 + + # Install from downloaded packages (air-gapped) + sudo $SCRIPT_NAME --offline --yes + + # Force EE edition + sudo $SCRIPT_NAME --edition ee --yes +EOF +} + +detect_os() { + if [[ -f /etc/os-release ]]; then + # shellcheck disable=SC1091 + source /etc/os-release + case "$ID" in + ubuntu|debian) + OS_FAMILY="debian" + PKG_MANAGER="apt" + ;; + rhel|centos|rocky|almalinux|ol|fedora) + OS_FAMILY="rhel" + if command -v dnf >/dev/null 2>&1; then + PKG_MANAGER="dnf" + else + PKG_MANAGER="yum" + fi + ;; + *) + log_error "Unsupported OS: $ID" + exit 1 + ;; + esac + else + log_error "Cannot detect OS — /etc/os-release not found" + exit 1 + fi + debug_echo "Detected OS: $OS_FAMILY ($PKG_MANAGER)" +} + +detect_edition() { + if [[ -n "$EDITION" ]]; then + debug_echo "Edition set by flag: $EDITION" + return + fi + + if command -v dpkg >/dev/null 2>&1; then + if dpkg -l gitlab-ee 2>/dev/null | grep -q "^ii"; then + EDITION="ee" + elif dpkg -l gitlab-ce 2>/dev/null | grep -q "^ii"; then + EDITION="ce" + fi + elif command -v rpm >/dev/null 2>&1; then + if rpm -q gitlab-ee >/dev/null 2>&1; then + EDITION="ee" + elif rpm -q gitlab-ce >/dev/null 2>&1; then + EDITION="ce" + fi + fi + + if [[ -z "$EDITION" ]]; then + log_error "Cannot detect GitLab edition. Use --edition ce|ee" + exit 1 + fi + debug_echo "Detected edition: $EDITION" +} + +get_current_version() { + local version_file="/opt/gitlab/embedded/service/gitlab-rails/VERSION" + if [[ -f "$version_file" ]]; then + CURRENT_VERSION=$(cat "$version_file") + else + log_error "GitLab does not appear to be installed ($version_file not found)" + exit 1 + fi + debug_echo "Current version: $CURRENT_VERSION" +} + +version_compare() { + # Returns 0 if $1 < $2, 1 if equal, 2 if $1 > $2 + if [[ "$1" == "$2" ]]; then + echo 1 + return + fi + + local IFS=. + local i + read -ra ver1 <<< "$1" + read -ra ver2 <<< "$2" + + for ((i = 0; i < ${#ver1[@]}; i++)); do + local v1=${ver1[i]:-0} + local v2=${ver2[i]:-0} + if ((v1 < v2)); then + echo 0 + return + elif ((v1 > v2)); then + echo 2 + return + fi + done + echo 1 +} + +get_upgrade_path() { + local current="$1" + local target="$2" + local path=() + + for stop in "${VERSION_STOPS[@]}"; do + local cmp_current + cmp_current=$(version_compare "$current" "$stop") + if [[ "$cmp_current" == "0" ]]; then + # Current < stop, so this stop is needed + if [[ -n "$target" ]]; then + local cmp_target + cmp_target=$(version_compare "$stop" "$target") + if [[ "$cmp_target" == "0" || "$cmp_target" == "1" ]]; then + path+=("$stop") + fi + else + path+=("$stop") + fi + fi + done + + # If target is specified and not already in the path, add it + if [[ -n "$target" ]]; then + local last="${path[*]: -1}" + if [[ "$last" != "$target" ]]; then + local in_stops=false + for stop in "${VERSION_STOPS[@]}"; do + if [[ "$stop" == "$target" ]]; then + in_stops=true + break + fi + done + if [[ "$in_stops" == false ]]; then + path+=("$target") + fi + fi + fi + + echo "${path[@]}" +} + +format_package_name() { + local version="$1" + if [[ "$OS_FAMILY" == "debian" ]]; then + echo "gitlab-${EDITION}=${version}-${EDITION}.0" + else + echo "gitlab-${EDITION}-${version}-${EDITION}.0" + fi +} + +format_package_filename() { + local version="$1" + if [[ "$OS_FAMILY" == "debian" ]]; then + echo "gitlab-${EDITION}_${version}-${EDITION}.0_amd64.deb" + else + echo "gitlab-${EDITION}-${version}-${EDITION}.0.el*.x86_64.rpm" + fi +} + +check_disk_space() { + local check_path="$1" + local required_gb="$2" + + local available_kb + available_kb=$(df --output=avail "$check_path" 2>/dev/null | tail -1 | tr -d ' ') + local available_gb=$((available_kb / 1024 / 1024)) + + if ((available_gb < required_gb)); then + log_error "Insufficient disk space on $check_path: ${available_gb}GB available, ${required_gb}GB required" + return 1 + fi + debug_echo "Disk space on $check_path: ${available_gb}GB available (${required_gb}GB required)" + return 0 +} + +check_background_migrations() { + log_info "Checking background migrations..." + + local pending + pending=$(sudo gitlab-rails runner -e production \ + 'puts Gitlab::Database::BackgroundMigration::BatchedMigration.queued.count' 2>/dev/null) || { + log_warn "Could not check background migrations via Rails runner" + return 0 + } + + if [[ "$pending" != "0" ]]; then + log_warn "$pending background migration(s) still pending" + log_info "Attempting to finalize background migrations..." + sudo gitlab-rake db:background_migrations:finalize + + local elapsed=0 + while ((elapsed < MIGRATION_TIMEOUT)); do + pending=$(sudo gitlab-rails runner -e production \ + 'puts Gitlab::Database::BackgroundMigration::BatchedMigration.queued.count' 2>/dev/null) || break + if [[ "$pending" == "0" ]]; then + log_info "All background migrations complete" + return 0 + fi + log_info "Waiting for $pending migration(s)... (${elapsed}s / ${MIGRATION_TIMEOUT}s)" + sleep "$MIGRATION_POLL_INTERVAL" + elapsed=$((elapsed + MIGRATION_POLL_INTERVAL)) + done + + if [[ "$pending" != "0" ]]; then + log_error "Background migrations did not complete within ${MIGRATION_TIMEOUT}s" + log_error "Run: sudo gitlab-rake db:background_migrations:status" + return 1 + fi + else + log_info "No pending background migrations" + fi + return 0 +} + +create_backup() { + if [[ "$SKIP_BACKUP" == true ]]; then + log_warn "Skipping backup (--skip-backup specified)" + return 0 + fi + + log_info "Creating GitLab backup..." + local backup_name + backup_name="pre-upgrade-$(date +%Y%m%d-%H%M%S)" + + check_disk_space "$BACKUP_DIR" "$MIN_DISK_GB" || return 1 + + sudo gitlab-backup create BACKUP="$backup_name" || { + log_error "Backup failed" + return 1 + } + + # Back up config files + log_info "Backing up configuration files..." + sudo cp /etc/gitlab/gitlab.rb "${BACKUP_DIR}/gitlab.rb.pre-upgrade.bak" + sudo cp /etc/gitlab/gitlab-secrets.json "${BACKUP_DIR}/gitlab-secrets.json.pre-upgrade.bak" + + log_info "Backup complete: $backup_name" + return 0 +} + +verify_health() { + local version="$1" + log_info "Verifying GitLab health after upgrade to $version..." + + # Check service status + if ! sudo gitlab-ctl status >/dev/null 2>&1; then + log_error "gitlab-ctl status reports unhealthy services" + sudo gitlab-ctl status + return 1 + fi + log_info "All services running" + + # Verify installed version + local installed + installed=$(cat /opt/gitlab/embedded/service/gitlab-rails/VERSION 2>/dev/null) + if [[ "$installed" != "$version" ]]; then + log_warn "Expected version $version but found $installed" + else + log_info "Version confirmed: $installed" + fi + + # Run health check + log_info "Running gitlab:check..." + if ! sudo gitlab-rake gitlab:check SANITIZE=true >/dev/null 2>&1; then + log_warn "gitlab:check reported issues — review manually" + else + log_info "Health check passed" + fi + + return 0 +} + +install_package_online() { + local version="$1" + local pkg_name + pkg_name=$(format_package_name "$version") + + log_info "Installing $pkg_name (online)..." + + case "$PKG_MANAGER" in + apt) + sudo apt-get update -qq || true + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y "$pkg_name" || return 1 + ;; + dnf) + sudo dnf install -y "$pkg_name" || return 1 + ;; + yum) + sudo yum install -y "$pkg_name" || return 1 + ;; + esac + return 0 +} + +install_package_offline() { + local version="$1" + local pattern + pattern=$(format_package_filename "$version") + + # Find the package file + local pkg_file + pkg_file=$(find "$PACKAGE_DIR" -name "$pattern" -type f 2>/dev/null | head -1) + + if [[ -z "$pkg_file" ]]; then + log_error "Package not found in $PACKAGE_DIR matching: $pattern" + log_error "Run --download-only first to fetch packages" + return 1 + fi + + log_info "Installing from local package: $pkg_file" + + case "$OS_FAMILY" in + debian) + sudo DEBIAN_FRONTEND=noninteractive dpkg -i "$pkg_file" || { + log_info "Resolving dependencies..." + sudo apt-get install -f -y || return 1 + } + ;; + rhel) + if [[ "$PKG_MANAGER" == "dnf" ]]; then + sudo dnf install -y "$pkg_file" || return 1 + else + sudo yum localinstall -y "$pkg_file" || return 1 + fi + ;; + esac + return 0 +} + +download_packages() { + local -a path + IFS=' ' read -ra path <<< "$(get_upgrade_path "$CURRENT_VERSION" "$TARGET_VERSION")" + + if [[ ${#path[@]} -eq 0 ]]; then + log_info "No upgrades needed — already at or above target version" + return 0 + fi + + mkdir -p "$PACKAGE_DIR" + + log_info "Downloading ${#path[@]} package(s) to $PACKAGE_DIR" + log_info "Upgrade path: ${path[*]}" + + local failed=0 + for version in "${path[@]}"; do + local pkg_name + pkg_name=$(format_package_name "$version") + log_info "Downloading: $pkg_name" + + case "$PKG_MANAGER" in + apt) + (cd "$PACKAGE_DIR" && sudo apt-get download "$pkg_name" 2>/dev/null) || { + # Try apt-get with download option + sudo apt-get install --download-only -y -o Dir::Cache::Archives="$PACKAGE_DIR" "$pkg_name" 2>/dev/null || { + log_error "Failed to download $pkg_name" + failed=$((failed + 1)) + } + } + ;; + dnf) + sudo dnf download --destdir="$PACKAGE_DIR" "$pkg_name" || { + log_error "Failed to download $pkg_name" + failed=$((failed + 1)) + } + ;; + yum) + sudo yumdownloader --destdir="$PACKAGE_DIR" "$pkg_name" || { + log_error "Failed to download $pkg_name" + failed=$((failed + 1)) + } + ;; + esac + done + + if ((failed > 0)); then + log_error "$failed package(s) failed to download" + return 1 + fi + + log_info "All packages downloaded to $PACKAGE_DIR" + log_info "Transfer this directory to your air-gapped system, then run:" + log_info " PACKAGE_DIR=$PACKAGE_DIR $SCRIPT_NAME --offline" + return 0 +} + +list_stops() { + local -a path + IFS=' ' read -ra path <<< "$(get_upgrade_path "$CURRENT_VERSION" "$TARGET_VERSION")" + + echo "Current version: $CURRENT_VERSION" + echo "Edition: gitlab-$EDITION" + echo "OS: $OS_FAMILY ($PKG_MANAGER)" + if [[ -n "$TARGET_VERSION" ]]; then + echo "Target version: $TARGET_VERSION" + else + echo "Target version: latest (${VERSION_STOPS[-1]})" + fi + echo "" + + if [[ ${#path[@]} -eq 0 ]]; then + echo "No upgrades needed — already at or above target version." + return + fi + + echo "Required upgrade path (${#path[@]} stops):" + echo "" + echo " $CURRENT_VERSION (current)" + for stop in "${path[@]}"; do + echo " → $stop" + done + echo "" + echo "Estimated time: $((${#path[@]} * 15))-$((${#path[@]} * 30)) minutes (varies by database size)" +} + +run_upgrade() { + local -a path + IFS=' ' read -ra path <<< "$(get_upgrade_path "$CURRENT_VERSION" "$TARGET_VERSION")" + + if [[ ${#path[@]} -eq 0 ]]; then + log_info "No upgrades needed — already at or above target version" + return 0 + fi + + local offline=false + if [[ "$MODE" == "offline" ]]; then + offline=true + fi + + echo "============================================" + echo " GitLab Upgrade: $CURRENT_VERSION → ${path[-1]}" + echo " Edition: gitlab-$EDITION" + echo " Stops: ${#path[@]}" + echo " Mode: $(if $offline; then echo "offline"; else echo "online"; fi)" + echo "============================================" + echo "" + + if [[ "$AUTO_YES" != true ]]; then + echo "Press Enter to start the upgrade, or Ctrl+C to abort..." + read -r + fi + + # Pre-upgrade checks + check_disk_space "/opt/gitlab" "$MIN_DISK_GB" || exit 1 + check_disk_space "/var/opt/gitlab" "$MIN_DISK_GB" || exit 1 + check_disk_space "/tmp" 1 || exit 1 + + # Create backup before starting + create_backup || exit 1 + + # Check initial background migrations + check_background_migrations || exit 1 + + local stop_num=0 + local total=${#path[@]} + for version in "${path[@]}"; do + stop_num=$((stop_num + 1)) + echo "" + echo "============================================" + echo " Stop $stop_num/$total: Upgrading to $version" + echo "============================================" + + # Check background migrations before this stop + if ((stop_num > 1)); then + check_background_migrations || { + log_error "Cannot proceed — background migrations incomplete" + exit 1 + } + fi + + # Install the package + if $offline; then + install_package_offline "$version" || { + log_error "Failed to install $version from local packages" + exit 1 + } + else + install_package_online "$version" || { + log_error "Failed to install $version" + exit 1 + } + fi + + # Verify health + verify_health "$version" || { + log_warn "Health verification had issues — review before continuing" + } + + # Check for new background migrations + log_info "Checking for new background migrations..." + sudo gitlab-rake db:background_migrations:status 2>/dev/null || true + + if ((stop_num < total)); then + if [[ "$AUTO_YES" != true ]]; then + echo "" + echo "Stop $stop_num/$total complete. Press Enter for next stop, or Ctrl+C to abort..." + read -r + else + log_info "Stop $stop_num/$total complete — proceeding to next stop" + fi + fi + done + + echo "" + echo "============================================" + echo " Upgrade Complete" + echo "============================================" + local final_version + final_version=$(cat /opt/gitlab/embedded/service/gitlab-rails/VERSION 2>/dev/null) + echo " Final version: $final_version" + echo "" + echo "Post-upgrade tasks:" + echo " 1. sudo gitlab-rake gitlab:check SANITIZE=true" + echo " 2. sudo gitlab-rake db:background_migrations:status" + echo " 3. Verify CI/CD pipelines and runners" + echo " 4. Check container registry functionality" + echo " 5. Remove maintenance notification" +} + +parse_arguments() { + while [[ $# -gt 0 ]]; do + case $1 in + --download-only) + MODE="download" + shift + ;; + --offline) + MODE="offline" + shift + ;; + --list-stops) + MODE="list" + shift + ;; + --target) + TARGET_VERSION="$2" + shift 2 + ;; + --edition) + EDITION="$2" + if [[ "$EDITION" != "ce" && "$EDITION" != "ee" ]]; then + log_error "Edition must be 'ce' or 'ee'" + exit 1 + fi + shift 2 + ;; + --yes) + AUTO_YES=true + shift + ;; + --skip-backup) + SKIP_BACKUP=true + shift + ;; + --help|-h) + show_help + exit 0 + ;; + *) + log_error "Unknown option: $1" + show_help >&2 + exit 1 + ;; + esac + done +} + +validate_requirements() { + # Must run as root for upgrade/download operations + if [[ "$MODE" != "list" && $EUID -ne 0 ]]; then + log_error "This script must be run as root (use sudo)" + exit 1 + fi + + detect_os + detect_edition + get_current_version + + # Validate target version format if specified + if [[ -n "$TARGET_VERSION" ]]; then + if ! [[ "$TARGET_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + log_error "Invalid target version format: $TARGET_VERSION (expected: X.Y.Z)" + exit 1 + fi + fi + + # For offline mode, verify package directory exists + if [[ "$MODE" == "offline" ]]; then + if [[ ! -d "$PACKAGE_DIR" ]]; then + log_error "Package directory not found: $PACKAGE_DIR" + log_error "Run --download-only first, then transfer packages to this directory" + exit 1 + fi + fi +} + +main() { + parse_arguments "$@" + + # Allow --list-stops and --help without root + if [[ "$MODE" == "list" ]]; then + detect_os + detect_edition + get_current_version + list_stops + exit 0 + fi + + validate_requirements + + case "$MODE" in + download) + download_packages + ;; + upgrade|offline) + run_upgrade + ;; + esac + + debug_echo "Script completed successfully" +} + +# Execute main function if script is run directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/networktuning.sh b/networktuning.sh new file mode 100644 index 0000000..39c02df --- /dev/null +++ b/networktuning.sh @@ -0,0 +1,398 @@ +#!/bin/bash + +###################################################################################### +#### Version 2.00 #### +#### For questions or comments contact@mylinux.work #### +#### Author : Phil Connor #### +#### #### +#### Notes : #### +#### This script configures sysctl network and system tuning on Linux servers. #### +#### It writes an idempotent drop-in file to /etc/sysctl.d/ so it can be run #### +#### multiple times safely without duplicating settings. #### +#### #### +#### Use this script at your OWN risk. There is no guarantee whatsoever. #### +#### #### +#### Usage: #### +#### networktuning.sh - Standard tuning (HDD) #### +#### networktuning.sh ssd - SSD-optimized dirty page settings #### +#### networktuning.sh nvme - NVMe-optimized dirty page settings #### +#### networktuning.sh --bbr - Enable BBR congestion control #### +#### networktuning.sh ssd --bbr - SSD + BBR #### +#### networktuning.sh --router - Enable IP forwarding #### +#### networktuning.sh --dry-run - Show config without applying #### +#### networktuning.sh --rollback - Restore previous configuration #### +###################################################################################### + +set -euo pipefail + +########################## +#### System Variables #### +########################## +HOST=$(hostname) +CONF_FILE="/etc/sysctl.d/99-network-tuning.conf" +BACKUP_DIR="/etc/sysctl.d/backups" +STORAGE_TYPE="hdd" +ENABLE_BBR=false +ENABLE_ROUTER=false +DRY_RUN=false +ROLLBACK=false + +######################### +#### Color Variables #### +######################### +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +###################### +#### Root Check #### +###################### +if [[ $EUID -ne 0 ]]; then + echo -e "${RED}Error: This script must be run as root${NC}" + exit 1 +fi + +############################## +#### Parse CLI Arguments #### +############################## +for arg in "$@"; do + case "$arg" in + ssd) STORAGE_TYPE="ssd" ;; + nvme) STORAGE_TYPE="nvme" ;; + --bbr) ENABLE_BBR=true ;; + --router) ENABLE_ROUTER=true ;; + --dry-run) DRY_RUN=true ;; + --rollback) ROLLBACK=true ;; + --help|-h) + echo "Usage: $(basename "$0") [ssd|nvme] [--bbr] [--router] [--dry-run] [--rollback]" + echo "" + echo "Options:" + echo " ssd Optimize dirty page settings for SSD storage" + echo " nvme Optimize dirty page settings for NVMe storage" + echo " --bbr Enable BBR congestion control (requires kernel 4.9+)" + echo " --router Enable IP forwarding (IPv4 and IPv6)" + echo " --dry-run Display the configuration without applying it" + echo " --rollback Restore the previous configuration backup" + echo " --help Show this help message" + exit 0 + ;; + *) + echo -e "${RED}Unknown option: $arg${NC}" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + +################################## +#### Detect OS and OS Version #### +################################## +if [[ ! -f /etc/os-release ]]; then + echo -e "${RED}Error: /etc/os-release not found. Unsupported system.${NC}" + exit 1 +fi + +OS_ID=$(. /etc/os-release && echo "${ID}") +OS_VERSION=$(. /etc/os-release && echo "${VERSION_ID}" | cut -d. -f1) + +case "$OS_ID" in + ubuntu|debian) + PAKMGR="apt-get -y" + ;; + centos|rhel|oracle|rocky|almalinux|fedora) + if [[ "$OS_VERSION" -le 7 ]]; then + PAKMGR="yum -y" + else + PAKMGR="dnf -y" + fi + ;; + *) + echo -e "${YELLOW}Warning: Unrecognized OS '${OS_ID}'. Proceeding without package manager.${NC}" + PAKMGR="" + ;; +esac + +################################ +#### Handle Rollback #### +################################ +if [[ "$ROLLBACK" == true ]]; then + LATEST_BACKUP=$(find "$BACKUP_DIR" -name "99-network-tuning.conf.*" -type f 2>/dev/null | sort -r | head -1) + if [[ -z "$LATEST_BACKUP" ]]; then + echo -e "${RED}Error: No backup found in $BACKUP_DIR${NC}" + exit 1 + fi + echo -e "${GREEN}Restoring from: $LATEST_BACKUP${NC}" + cp "$LATEST_BACKUP" "$CONF_FILE" + sysctl --system > /dev/null 2>&1 + echo -e "${GREEN}Rollback complete. Settings applied.${NC}" + exit 0 +fi + +########################## +#### Sysctl Variables #### +########################## +MEM_BYTES=$(awk '/MemTotal:/ { printf "%0.f",$2 * 1024}' /proc/meminfo) +MEM_KB=$(awk '/MemTotal:/ { printf "%0.f",$2}' /proc/meminfo) +MAX_ORPHAN=$((MEM_BYTES / 10 / 65536)) +FILE_MAX=$((MEM_BYTES / 4194304 * 256)) +MAX_TW=$((FILE_MAX * 2)) +MIN_FREE=$((MEM_KB / 100)) + +###################################### +#### Storage-based dirty settings #### +###################################### +case "$STORAGE_TYPE" in + hdd) + VM_DIRTY_BG_RATIO=5 + VM_DIRTY_RATIO=15 + ;; + ssd) + VM_DIRTY_BG_RATIO=3 + VM_DIRTY_RATIO=5 + ;; + nvme) + VM_DIRTY_BG_RATIO=1 + VM_DIRTY_RATIO=3 + ;; +esac + +#################################### +#### BBR availability check #### +#################################### +if [[ "$ENABLE_BBR" == true ]]; then + if ! modprobe tcp_bbr 2>/dev/null; then + echo -e "${YELLOW}Warning: BBR kernel module not available. Skipping BBR configuration.${NC}" + ENABLE_BBR=false + fi +fi + +################################# +#### Generate Configuration #### +################################# +echo "#######################################" +echo "#### Network Tuning for $HOST" +echo "#### Storage: $STORAGE_TYPE | BBR: $ENABLE_BBR | Router: $ENABLE_ROUTER" +echo "#######################################" + +CONFIG=$(cat << EOF +############################################################################### +# Network Tuning Configuration +# Generated: $(date '+%Y-%m-%d %H:%M:%S') +# Host: $HOST +# Storage: $STORAGE_TYPE | BBR: $ENABLE_BBR | Router: $ENABLE_ROUTER +# Generator: networktuning.sh v2.00 +############################################################################### + +############################ +#### Performance Tuning #### +############################ + +# Enable SYN cookies for SYN flood protection +net.ipv4.tcp_syncookies = 1 + +# Basic TCP tuning +net.ipv4.tcp_keepalive_time = 600 +net.ipv4.tcp_synack_retries = 3 +net.ipv4.tcp_syn_retries = 3 + +# RFC1337 — handle TIME-WAIT assassination hazards +net.ipv4.tcp_rfc1337 = 1 + +# Defines the local port range used by TCP and UDP +net.ipv4.ip_local_port_range = 1024 65535 + +# Log Martian packets with impossible addresses +net.ipv4.conf.all.log_martians = 1 +net.ipv4.conf.default.log_martians = 1 + +# Enable window scaling (RFC1323) +net.ipv4.tcp_window_scaling = 1 + +# Enable timestamps (RFC1323) +net.ipv4.tcp_timestamps = 1 + +# Enable selective acknowledgments +net.ipv4.tcp_sack = 1 + +# Allow TCP to send duplicate SACKs +net.ipv4.tcp_dsack = 1 + +# Loose reverse path filtering (compatible with cloud/multihomed) +net.ipv4.conf.default.rp_filter = 2 +net.ipv4.conf.all.rp_filter = 2 + +# Max remembered connection requests in SYN backlog +net.ipv4.tcp_max_syn_backlog = 20000 + +# Max orphaned TCP sockets (calculated from system memory) +net.ipv4.tcp_max_orphans = $MAX_ORPHAN + +# Retries before killing an orphaned TCP connection +net.ipv4.tcp_orphan_retries = 1 + +# Time to hold sockets in FIN-WAIT-2 state (seconds) +net.ipv4.tcp_fin_timeout = 20 + +# Maximum TIME-WAIT sockets held simultaneously +net.ipv4.tcp_max_tw_buckets = $MAX_TW + +# Don't cache ssthresh from previous connections +net.ipv4.tcp_no_metrics_save = 1 + +# Enable receive buffer auto-tuning +net.ipv4.tcp_moderate_rcvbuf = 1 + +# TCP buffer auto-tuning limits (min default max bytes) +net.ipv4.tcp_rmem = 4096 87380 16777216 +net.ipv4.tcp_wmem = 4096 65536 16777216 + +# Max socket buffer sizes +net.core.rmem_max = 16777216 +net.core.wmem_max = 16777216 + +# Network device backlog queue length +net.core.netdev_max_backlog = 2500 + +# Max pending connections in listen queue +net.core.somaxconn = 65000 + +############################ +#### VM / Storage Tuning ### +############################ + +# Prefer keeping processes in memory over swapping (1 = minimal swap) +vm.swappiness = 1 + +# Dirty page writeback ratios (tuned for $STORAGE_TYPE) +# Monitor with: grep -A 1 dirty /proc/vmstat +vm.dirty_background_ratio = $VM_DIRTY_BG_RATIO +vm.dirty_ratio = $VM_DIRTY_RATIO + +# Required free memory (1% of physical RAM) +vm.min_free_kbytes = $MIN_FREE + +# System open file limit (calculated from system memory) +fs.file-max = $FILE_MAX + +############################ +#### Kernel Settings #### +############################ + +# Kernel log levels +kernel.printk = 4 4 1 7 +kernel.core_uses_pid = 1 +kernel.sysrq = 0 + +# Disable core dumps for setuid programs +fs.suid_dumpable = 0 + +########################### +#### Security Settings #### +########################### + +# Full ASLR (randomize stack, VDSO, mmap, heap) +kernel.randomize_va_space = 2 + +# Don't accept ICMP redirects +net.ipv4.conf.all.accept_redirects = 0 +net.ipv4.conf.default.accept_redirects = 0 +net.ipv6.conf.all.accept_redirects = 0 +net.ipv6.conf.default.accept_redirects = 0 + +# Don't send ICMP redirects +net.ipv4.conf.all.send_redirects = 0 +net.ipv4.conf.default.send_redirects = 0 + +# Don't accept IP source route packets +net.ipv4.conf.all.accept_source_route = 0 +net.ipv4.conf.default.accept_source_route = 0 +net.ipv6.conf.all.accept_source_route = 0 + +# Ignore ICMP broadcasts and bogus error responses +net.ipv4.icmp_echo_ignore_broadcasts = 1 +net.ipv4.icmp_ignore_bogus_error_responses = 1 + +# IPv6 router advertisement hardening +net.ipv6.conf.default.accept_ra_rtr_pref = 0 +net.ipv6.conf.all.accept_ra_rtr_pref = 0 +net.ipv6.conf.default.accept_ra_pinfo = 0 +net.ipv6.conf.all.accept_ra_pinfo = 0 +net.ipv6.conf.default.accept_ra_defrtr = 0 +net.ipv6.conf.all.accept_ra_defrtr = 0 +EOF +) + +# Append BBR congestion control if enabled +if [[ "$ENABLE_BBR" == true ]]; then + CONFIG+=$(cat << 'EOF' + + +########################### +#### BBR Congestion #### +########################### + +# Use fair queuing packet scheduler (required for BBR) +net.core.default_qdisc = fq + +# Enable BBR congestion control algorithm +net.ipv4.tcp_congestion_control = bbr +EOF +) +fi + +# Append router/forwarding settings if enabled +if [[ "$ENABLE_ROUTER" == true ]]; then + CONFIG+=$(cat << 'EOF' + + +########################### +#### Router / Forward #### +########################### + +# Enable IP forwarding (IPv4 and IPv6) +net.ipv4.ip_forward = 1 +net.ipv6.conf.all.forwarding = 1 +EOF +) +fi + +############################## +#### Apply Configuration #### +############################## +if [[ "$DRY_RUN" == true ]]; then + echo "" + echo -e "${YELLOW}--- Dry Run: Configuration Preview ---${NC}" + echo "$CONFIG" + echo -e "${YELLOW}--- End Preview ---${NC}" + exit 0 +fi + +# Create backup directory +mkdir -p "$BACKUP_DIR" + +# Backup existing config if present +if [[ -f "$CONF_FILE" ]]; then + cp "$CONF_FILE" "${BACKUP_DIR}/99-network-tuning.conf.$(date +%Y%m%d-%H%M%S)" + echo -e "${GREEN}Backed up existing config to $BACKUP_DIR${NC}" +fi + +# Write configuration atomically +TEMP_FILE=$(mktemp) +echo "$CONFIG" > "$TEMP_FILE" + +# Validate before installing +if sysctl -p "$TEMP_FILE" > /dev/null 2>&1; then + mv "$TEMP_FILE" "$CONF_FILE" + chmod 644 "$CONF_FILE" + sysctl --system > /dev/null 2>&1 + echo -e "${GREEN}Network tuning applied successfully to $CONF_FILE${NC}" +else + echo -e "${RED}Error: Configuration validation failed. No changes applied.${NC}" + echo -e "${RED}Review errors above and check kernel compatibility.${NC}" + rm -f "$TEMP_FILE" + exit 1 +fi + +echo -e "${GREEN}Done. Use 'sysctl --system' to verify or '$(basename "$0") --rollback' to revert.${NC}" +exit 0