#!/bin/bash set -Eeuo pipefail # ───────────────────────────────────────────── # System Maintenance & Timezone Setup Script # Compatible: Debian 10-13, Ubuntu 18-24 # ───────────────────────────────────────────── # ── Colors ────────────────────────────────── C_ORANGE='\033[38;5;208m' C_GREEN='\033[38;5;47m' C_RED='\033[38;5;196m' C_CYAN='\033[38;5;51m' C_BOLD='\033[1m' C_RESET='\033[0m' # ── Log File ───────────────────────────────── LOG_FILE="/var/log/system-maintenance.log" SCRIPT_START=$(date '+%Y-%m-%d %H:%M:%S') REBOOT_NEEDED=false KERNEL_UPDATED=false # ── Trap ───────────────────────────────────── trap 'handle_error $? $LINENO' ERR handle_error() { local exit_code=$1 local line_num=$2 echo -e "\n${C_RED}${C_BOLD}✖ FATAL ERROR${C_RESET}${C_RED} — Exit code ${exit_code} at line ${line_num}${C_RESET}" log_entry "FATAL ERROR: exit code ${exit_code} at line ${line_num}" exit "${exit_code}" } # ── Logging ────────────────────────────────── log_entry() { local msg="$1" echo "[$(date '+%Y-%m-%d %H:%M:%S')] ${msg}" >> "${LOG_FILE}" 2>/dev/null || true } # ── Print Helpers ───────────────────────────── print_banner() { clear echo -e "${C_CYAN}${C_BOLD}" echo " ╔══════════════════════════════════════════════════════════╗" echo " ║ SYSTEM MAINTENANCE & TIMEZONE SETUP ║" echo " ║ Debian 10-13 • Ubuntu 18-24 ║" echo " ╚══════════════════════════════════════════════════════════╝" echo -e "${C_RESET}" echo -e " ${C_ORANGE}Started: ${SCRIPT_START}${C_RESET}" echo -e " ${C_ORANGE}Log: ${LOG_FILE}${C_RESET}\n" } print_section() { local title="$1" echo -e "\n${C_CYAN}${C_BOLD} ┌─────────────────────────────────────────────────┐${C_RESET}" printf "${C_CYAN}${C_BOLD} │ %-47s │${C_RESET}\n" "${title}" echo -e "${C_CYAN}${C_BOLD} └─────────────────────────────────────────────────┘${C_RESET}\n" } ok() { echo -e " ${C_GREEN}${C_BOLD}✔ Success:${C_RESET} $*"; log_entry "OK: $*"; } warn() { echo -e " ${C_ORANGE}${C_BOLD}⚠ Warning:${C_RESET} $*"; log_entry "WARN: $*"; } err() { echo -e " ${C_RED}${C_BOLD}✖ Error:${C_RESET} $*"; log_entry "ERROR: $*"; } info() { echo -e " ${C_ORANGE}○ ${C_RESET}$*"; log_entry "INFO: $*"; } # ── Spinner ─────────────────────────────────── spinner() { local pid=$1 local msg="${2:-Processing...}" local spin=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') local i=0 tput civis 2>/dev/null || true while kill -0 "${pid}" 2>/dev/null; do printf "\r ${C_ORANGE}${spin[$i]} ${msg}${C_RESET} " i=$(( (i + 1) % 10 )) sleep 0.1 done printf "\r%-60s\r" " " tput cnorm 2>/dev/null || true } # ── Progress Bar ────────────────────────────── progress_bar() { local current=$1 local total=$2 local label="${3:-Progress}" local width=40 local filled=$(( current * width / total )) local empty=$(( width - filled )) local bar bar="$(printf '%0.s█' $(seq 1 $filled 2>/dev/null || true))$(printf '%0.s░' $(seq 1 $empty 2>/dev/null || true))" local pct=$(( current * 100 / total )) printf "\r ${C_ORANGE}%-20s${C_RESET} [${C_GREEN}%s${C_RESET}] ${C_BOLD}%3d%%${C_RESET}" "${label}" "${bar}" "${pct}" } # ── Privilege Handling ──────────────────────── SUDO="" setup_privileges() { print_section "PRIVILEGE CHECK" if [[ "${EUID}" -eq 0 ]]; then ok "Running as root — no sudo needed" SUDO="" else info "Running as ${USER} — checking sudo access..." if sudo -n true 2>/dev/null; then ok "Passwordless sudo available" SUDO="sudo" elif sudo -v 2>/dev/null; then ok "Sudo access granted" SUDO="sudo" else err "No root or sudo access. Cannot continue." exit 1 fi fi } # ── OS Detection ────────────────────────────── OS_ID="" OS_CODENAME="" OS_VERSION="" detect_os() { print_section "OS DETECTION" if command -v lsb_release &>/dev/null; then OS_ID=$(lsb_release -si 2>/dev/null | tr '[:upper:]' '[:lower:]') OS_CODENAME=$(lsb_release -sc 2>/dev/null | tr '[:upper:]' '[:lower:]') OS_VERSION=$(lsb_release -sr 2>/dev/null) elif [[ -f /etc/os-release ]]; then # shellcheck source=/dev/null source /etc/os-release OS_ID=$(echo "${ID:-unknown}" | tr '[:upper:]' '[:lower:]') OS_CODENAME=$(echo "${VERSION_CODENAME:-unknown}" | tr '[:upper:]' '[:lower:]') OS_VERSION="${VERSION_ID:-unknown}" else err "Cannot detect OS — /etc/os-release missing and lsb_release unavailable" exit 1 fi case "${OS_ID}" in debian) local major_ver major_ver=$(echo "${OS_VERSION}" | cut -d. -f1) if [[ "${major_ver}" -ge 10 && "${major_ver}" -le 13 ]]; then ok "Detected: Debian ${OS_VERSION} (${OS_CODENAME})" else err "Unsupported Debian version: ${OS_VERSION}. Supported: 10–13" exit 1 fi ;; ubuntu) local major_ver major_ver=$(echo "${OS_VERSION}" | cut -d. -f1) if [[ "${major_ver}" -ge 18 && "${major_ver}" -le 24 ]]; then ok "Detected: Ubuntu ${OS_VERSION} (${OS_CODENAME})" else err "Unsupported Ubuntu version: ${OS_VERSION}. Supported: 18–24" exit 1 fi ;; *) err "Unsupported OS: '${OS_ID}'. This script supports Debian and Ubuntu only." exit 1 ;; esac log_entry "OS detected: ${OS_ID} ${OS_VERSION} (${OS_CODENAME})" } # ── Dependency Check ────────────────────────── install_dep() { local pkg="$1" if ! dpkg -l "${pkg}" &>/dev/null || ! dpkg -l "${pkg}" | grep -q '^ii'; then info "Installing missing dependency: ${pkg}" ${SUDO} env DEBIAN_FRONTEND=noninteractive apt-get install -y "${pkg}" -qq > /dev/null 2>&1 & local pid=$! spinner "${pid}" "Installing ${pkg}..." wait "${pid}" ok "Installed: ${pkg}" log_entry "Installed dependency: ${pkg}" else ok "Dependency satisfied: ${pkg}" fi } check_deps() { print_section "DEPENDENCY CHECK" info "Updating apt cache silently..." ${SUDO} apt-get update -qq > /dev/null 2>&1 & local pid=$! spinner "${pid}" "Refreshing apt cache..." wait "${pid}" ok "Apt cache refreshed" local deps=("lsb-release" "pv" "systemd-timesyncd") for dep in "${deps[@]}"; do install_dep "${dep}" done # Ensure timedatectl is available (part of systemd) if ! command -v timedatectl &>/dev/null; then err "timedatectl not found — systemd required" exit 1 fi ok "timedatectl available" # Ensure systemd is the init system if [[ ! -d /run/systemd/system ]]; then err "systemd is not the active init system — incompatible environment" exit 1 fi ok "systemd confirmed as init system" } # ── Apt Update ──────────────────────────────── run_apt_update() { print_section "SYSTEM UPDATE" echo -ne " ${C_ORANGE}Perform full system update? [Y/n]: ${C_RESET}" local answer read -r -t 30 answer || answer="Y" answer="${answer:-Y}" if [[ ! "${answer}" =~ ^[Yy]$ ]]; then warn "System update skipped by user" log_entry "System update: skipped" return 0 fi log_entry "System update: started" # ── Show upgradable packages ── echo "" info "Fetching upgradable package list..." local upgradable upgradable=$(apt list --upgradable 2>/dev/null | grep -v "^Listing" || true) if [[ -z "${upgradable}" ]]; then ok "System is already up to date — no packages to upgrade" log_entry "No packages to upgrade" else local pkg_count pkg_count=$(echo "${upgradable}" | wc -l) echo -e "\n ${C_CYAN}${C_BOLD}Packages to upgrade (${pkg_count}):${C_RESET}" echo -e " ${C_CYAN}──────────────────────────────────────────────────${C_RESET}" # Check if kernel packages are being updated if echo "${upgradable}" | grep -q 'linux-image\|linux-headers\|linux-modules'; then KERNEL_UPDATED=true warn "Kernel update detected — reboot will be recommended" fi # Format: package_name current → new while IFS= read -r line; do local pkg_name pkg_new pkg_arch pkg_name=$(echo "${line}" | awk -F'/' '{print $1}') pkg_new=$(echo "${line}" | awk '{print $2}') pkg_arch=$(echo "${line}" | awk '{print $3}') printf " ${C_ORANGE} %-35s${C_RESET} → ${C_GREEN}%s${C_RESET} ${pkg_arch}\n" "${pkg_name}" "${pkg_new}" log_entry "Upgrading: ${pkg_name} → ${pkg_new}" done <<< "${upgradable}" echo -e " ${C_CYAN}──────────────────────────────────────────────────${C_RESET}\n" # ── apt upgrade ── info "Running apt upgrade..." local steps=4 progress_bar 0 ${steps} "apt upgrade " echo "" ${SUDO} env DEBIAN_FRONTEND=noninteractive apt-get upgrade -y \ -o Dpkg::Progress-Fancy=0 \ -o APT::Color=0 \ -qq > /tmp/apt_upgrade_out.log 2>&1 & local pid=$! spinner "${pid}" "Upgrading packages..." wait "${pid}" progress_bar 1 ${steps} "apt upgrade "; echo "" ok "apt upgrade completed" # ── apt dist-upgrade (for Debian-style dependency resolution) ── ${SUDO} env DEBIAN_FRONTEND=noninteractive apt-get dist-upgrade -y \ -o Dpkg::Progress-Fancy=0 \ -o APT::Color=0 \ -qq > /tmp/apt_distupgrade_out.log 2>&1 & pid=$! spinner "${pid}" "Running dist-upgrade..." wait "${pid}" progress_bar 2 ${steps} "dist-upgrade "; echo "" ok "dist-upgrade completed" # ── autoremove ── ${SUDO} env DEBIAN_FRONTEND=noninteractive apt-get autoremove -y -qq \ > /tmp/apt_autoremove_out.log 2>&1 & pid=$! spinner "${pid}" "Running autoremove..." wait "${pid}" progress_bar 3 ${steps} "autoremove "; echo "" ok "autoremove completed" # ── autoclean ── ${SUDO} env DEBIAN_FRONTEND=noninteractive apt-get autoclean -y -qq \ > /tmp/apt_autoclean_out.log 2>&1 & pid=$! spinner "${pid}" "Running autoclean..." wait "${pid}" progress_bar 4 ${steps} "autoclean "; echo "" ok "autoclean completed" log_entry "System update: completed (${pkg_count} packages upgraded)" fi } # ── Timezone Setup ──────────────────────────── setup_timezone() { print_section "TIMEZONE CONFIGURATION" local current_tz current_tz=$(timedatectl show --property=Timezone --value 2>/dev/null || \ cat /etc/timezone 2>/dev/null || \ echo "Unknown") info "Current timezone: ${C_CYAN}${current_tz}${C_RESET}" echo "" echo -ne " ${C_ORANGE}Set timezone? Default: Europe/Athens [Y/n]: ${C_RESET}" local answer read -r -t 30 answer || answer="Y" answer="${answer:-Y}" if [[ ! "${answer}" =~ ^[Yy]$ ]]; then warn "Timezone change skipped" log_entry "Timezone: skipped" return 0 fi # ── Zone selection ── echo "" echo -e " ${C_CYAN}${C_BOLD}Common timezone options:${C_RESET}" echo -e " ${C_CYAN}──────────────────────────────────────────────────${C_RESET}" local zones=( "1) Europe/Athens (EET/EEST UTC+2/+3)" "2) Europe/London (GMT/BST UTC+0/+1)" "3) Europe/Berlin (CET/CEST UTC+1/+2)" "4) Europe/Paris (CET/CEST UTC+1/+2)" "5) America/New_York (EST/EDT UTC-5/-4)" "6) America/Chicago (CST/CDT UTC-6/-5)" "7) America/Denver (MST/MDT UTC-7/-6)" "8) America/Los_Angeles (PST/PDT UTC-8/-7)" "9) UTC" "0) Custom — enter manually" ) for z in "${zones[@]}"; do echo -e " ${C_ORANGE}${z}${C_RESET}" done echo -e " ${C_CYAN}──────────────────────────────────────────────────${C_RESET}" echo -ne "\n ${C_ORANGE}Select [1-9/0], default 1 (Europe/Athens): ${C_RESET}" local choice read -r -t 30 choice || choice="1" choice="${choice:-1}" local TZ_SELECTED="" case "${choice}" in 1|"") TZ_SELECTED="Europe/Athens" ;; 2) TZ_SELECTED="Europe/London" ;; 3) TZ_SELECTED="Europe/Berlin" ;; 4) TZ_SELECTED="Europe/Paris" ;; 5) TZ_SELECTED="America/New_York" ;; 6) TZ_SELECTED="America/Chicago" ;; 7) TZ_SELECTED="America/Denver" ;; 8) TZ_SELECTED="America/Los_Angeles" ;; 9) TZ_SELECTED="UTC" ;; 0) echo -ne " ${C_ORANGE}Enter timezone (e.g. Asia/Tokyo): ${C_RESET}" read -r TZ_SELECTED TZ_SELECTED="${TZ_SELECTED:-Europe/Athens}" ;; *) warn "Invalid choice — defaulting to Europe/Athens" TZ_SELECTED="Europe/Athens" ;; esac # ── Validate timezone file exists ── if [[ ! -f "/usr/share/zoneinfo/${TZ_SELECTED}" ]]; then err "Timezone '${TZ_SELECTED}' not found in /usr/share/zoneinfo/" exit 1 fi info "Applying timezone: ${C_GREEN}${TZ_SELECTED}${C_RESET}" log_entry "Setting timezone: ${TZ_SELECTED} (was: ${current_tz})" # ── Backup current timezone config ── local backup_ts backup_ts=$(date +%Y%m%d%H%M%S) if [[ -f /etc/timezone ]]; then if ${SUDO} cp /etc/timezone "/etc/timezone.bak.${backup_ts}" 2>/dev/null; then ok "Backup: /etc/timezone → /etc/timezone.bak.${backup_ts}" else warn "Backup of /etc/timezone failed (non-critical — continuing)" fi fi if [[ -f /etc/localtime || -L /etc/localtime ]]; then if ${SUDO} cp -aP /etc/localtime "/etc/localtime.bak.${backup_ts}" 2>/dev/null; then ok "Backup: /etc/localtime → /etc/localtime.bak.${backup_ts}" else warn "Backup of /etc/localtime failed (non-critical — continuing)" fi fi # ── Ubuntu 24.x unlock: disable NTP & restart timedated before set-timezone ── # On Ubuntu 24.x, systemd-timesyncd can hold /etc/localtime in a state # that makes `timedatectl set-timezone` fail silently. Dropping NTP and # bouncing systemd-timedated releases that lock. NTP gets re-enabled # later in setup_ntp(). ${SUDO} timedatectl set-ntp false 2>/dev/null || true ${SUDO} systemctl restart systemd-timedated 2>/dev/null || true # ── timedatectl (works on both Debian and Ubuntu) ── local cmd_out cmd_out=$(${SUDO} timedatectl set-timezone "${TZ_SELECTED}" 2>&1) || { err "timedatectl set-timezone failed: ${cmd_out}" exit 1 } ok "timedatectl set-timezone ${TZ_SELECTED}" # ── Debian/Ubuntu extra steps (belt-and-suspenders for /etc/timezone + tzdata) ── if [[ "${OS_ID}" == "debian" || "${OS_ID}" == "ubuntu" ]]; then info "Applying Debian/Ubuntu-specific timezone steps..." ${SUDO} ln -sf "/usr/share/zoneinfo/${TZ_SELECTED}" /etc/localtime || { err "Failed to create /etc/localtime symlink" exit 1 } ok "Symlink: /etc/localtime → /usr/share/zoneinfo/${TZ_SELECTED}" echo "${TZ_SELECTED}" | ${SUDO} tee /etc/timezone > /dev/null || { err "Failed to write /etc/timezone" exit 1 } ok "Written: /etc/timezone = ${TZ_SELECTED}" ${SUDO} env DEBIAN_FRONTEND=noninteractive dpkg-reconfigure -f noninteractive tzdata > /dev/null 2>&1 || { err "dpkg-reconfigure tzdata failed" exit 1 } ok "dpkg-reconfigure tzdata completed" fi # ── Verification ── echo "" info "Verification:" local verified_tz verified_tz=$(timedatectl show --property=Timezone --value 2>/dev/null || cat /etc/timezone) local current_date current_date=$(date) if [[ "${verified_tz}" == "${TZ_SELECTED}" ]]; then ok "Timezone confirmed: ${verified_tz}" ok "System clock: ${current_date}" log_entry "Timezone set: ${TZ_SELECTED} — verified OK" else err "Timezone mismatch! Expected: ${TZ_SELECTED}, Got: ${verified_tz}" exit 1 fi } # ── NTP Sync ────────────────────────────────── setup_ntp() { print_section "NTP TIME SYNCHRONIZATION" local current_ntp current_ntp=$(timedatectl show --property=NTP --value 2>/dev/null || echo "unknown") local current_sync current_sync=$(timedatectl show --property=NTPSynchronized --value 2>/dev/null || echo "unknown") info "NTP currently enabled: ${current_ntp}" info "NTP synchronized: ${current_sync}" echo "" echo -ne " ${C_ORANGE}Enable NTP sync with systemd-timesyncd? [Y/n]: ${C_RESET}" local answer read -r -t 30 answer || answer="Y" answer="${answer:-Y}" if [[ ! "${answer}" =~ ^[Yy]$ ]]; then warn "NTP sync setup skipped" log_entry "NTP: skipped" return 0 fi log_entry "NTP setup: started" # ── Ensure systemd-timesyncd is installed ── if ! systemctl list-unit-files systemd-timesyncd.service &>/dev/null; then info "Installing systemd-timesyncd..." install_dep "systemd-timesyncd" fi # ── Enable NTP via timedatectl ── local cmd_out cmd_out=$(${SUDO} timedatectl set-ntp true 2>&1) || { err "timedatectl set-ntp true failed: ${cmd_out}" exit 1 } ok "NTP enabled via timedatectl" # ── Enable and start systemd-timesyncd ── cmd_out=$(${SUDO} systemctl enable --now systemd-timesyncd 2>&1) || { err "Failed to enable/start systemd-timesyncd: ${cmd_out}" exit 1 } ok "systemd-timesyncd enabled and started" # ── Wait for sync (up to 15 seconds) ── info "Waiting for NTP synchronization (up to 15s)..." local synced=false for i in $(seq 1 15); do local sync_status sync_status=$(timedatectl show --property=NTPSynchronized --value 2>/dev/null || echo "no") if [[ "${sync_status}" == "yes" ]]; then synced=true break fi progress_bar "${i}" 15 "Syncing..." sleep 1 done echo "" if ${synced}; then ok "NTP synchronized successfully" else warn "NTP sync not confirmed within 15s — may still complete in background" fi # ── Show systemd-timesyncd status ── echo "" info "systemd-timesyncd status:" echo -e " ${C_CYAN}──────────────────────────────────────────────────${C_RESET}" systemctl status systemd-timesyncd --no-pager -l 2>/dev/null | \ grep -E "Loaded:|Active:|Status:|server:" | \ while IFS= read -r line; do echo -e " ${C_ORANGE}${line}${C_RESET}" done || true echo -e " ${C_CYAN}──────────────────────────────────────────────────${C_RESET}" # ── Final timedatectl status ── echo "" info "Final timedatectl status:" echo -e " ${C_CYAN}──────────────────────────────────────────────────${C_RESET}" timedatectl status 2>/dev/null | while IFS= read -r line; do echo -e " ${C_GREEN}${line}${C_RESET}" done echo -e " ${C_CYAN}──────────────────────────────────────────────────${C_RESET}" log_entry "NTP setup: completed and verified" } # ── Final Summary ───────────────────────────── show_summary() { print_section "FINAL SUMMARY" local end_time end_time=$(date '+%Y-%m-%d %H:%M:%S') local tz_now tz_now=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "N/A") local ntp_now ntp_now=$(timedatectl show --property=NTP --value 2>/dev/null || echo "N/A") local sync_now sync_now=$(timedatectl show --property=NTPSynchronized --value 2>/dev/null || echo "N/A") echo -e " ${C_CYAN}${C_BOLD}Script Results:${C_RESET}" echo -e " ${C_CYAN}──────────────────────────────────────────────────${C_RESET}" printf " ${C_ORANGE}%-30s${C_RESET} ${C_GREEN}%s${C_RESET}\n" "OS:" "${OS_ID^} ${OS_VERSION} (${OS_CODENAME})" printf " ${C_ORANGE}%-30s${C_RESET} ${C_GREEN}%s${C_RESET}\n" "Timezone:" "${tz_now}" printf " ${C_ORANGE}%-30s${C_RESET} ${C_GREEN}%s${C_RESET}\n" "NTP Enabled:" "${ntp_now}" printf " ${C_ORANGE}%-30s${C_RESET} ${C_GREEN}%s${C_RESET}\n" "NTP Synchronized:" "${sync_now}" printf " ${C_ORANGE}%-30s${C_RESET} ${C_GREEN}%s${C_RESET}\n" "Current Date/Time:" "$(date)" printf " ${C_ORANGE}%-30s${C_RESET} ${C_GREEN}%s${C_RESET}\n" "Log file:" "${LOG_FILE}" printf " ${C_ORANGE}%-30s${C_RESET} ${C_GREEN}%s${C_RESET}\n" "Completed at:" "${end_time}" echo -e " ${C_CYAN}──────────────────────────────────────────────────${C_RESET}" log_entry "Script completed at ${end_time}" # ── Reboot prompt if kernel was updated ── if ${KERNEL_UPDATED}; then echo "" echo -e " ${C_RED}${C_BOLD}⚠ Kernel packages were updated.${C_RESET}" echo -ne " ${C_ORANGE}Reboot now to apply kernel update? [y/N]: ${C_RESET}" local reboot_answer read -r -t 30 reboot_answer || reboot_answer="N" reboot_answer="${reboot_answer:-N}" if [[ "${reboot_answer}" =~ ^[Yy]$ ]]; then log_entry "Reboot initiated by user" ok "Rebooting in 5 seconds... (Ctrl+C to cancel)" sleep 5 ${SUDO} reboot else warn "Reboot skipped — please reboot manually to apply kernel update" log_entry "Reboot: skipped by user" fi fi echo "" echo -e " ${C_GREEN}${C_BOLD}✔ All done. System is configured and ready.${C_RESET}\n" } # ── Initialize Log ──────────────────────────── init_log() { ${SUDO:-} touch "${LOG_FILE}" 2>/dev/null || { LOG_FILE="/tmp/system-maintenance.log" touch "${LOG_FILE}" warn "Cannot write to /var/log — logging to ${LOG_FILE}" } ${SUDO:-} chmod 644 "${LOG_FILE}" 2>/dev/null || true log_entry "═══════════════════════════════════════════════════" log_entry "Script started: ${SCRIPT_START}" } # ── Main ────────────────────────────────────── main() { print_banner init_log setup_privileges detect_os check_deps run_apt_update setup_timezone setup_ntp show_summary } main "$@"