-
-
Save alsyundawy/d6e8e62e9409f2b45ecbe5e230f8f52d to your computer and use it in GitHub Desktop.
Upgrade Pnetlab from_any_to_5.3.11 with reboot without confirmation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ============================================================================== | |
| # PNETLab Upgrade Helper - Hardened Edition | |
| # ============================================================================== | |
| # File : upgrade-pnetlab-to-5.3.11.sh | |
| # Version : 2026.06.13-r1 | |
| # Date : 2026-06-13 | |
| # Target : PNETLab 5.x -> 5.3.11 | |
| # Maintainer : HARRY DERTIN SUTISNA ALSYUNDAWY | |
| # | |
| # Purpose: | |
| # Upgrade PNETLab to 5.3.11 using the official upgrade package workflow: | |
| # unzip package -> chmod -> dos2unix -> ./upgrade/upgrade -> reboot. | |
| # | |
| # Safety notes: | |
| # - Refuses to downgrade/upgrade PNETLab 6.x to 5.3.11. | |
| # - Refuses to run twice when current version is already 5.3.11. | |
| # - Validates downloaded/local ZIP before extracting. | |
| # - Creates a minimal database/config backup before running the vendor updater. | |
| # - Uses a lock directory to avoid concurrent execution. | |
| # - Reboots only after the upgrade command exits successfully. | |
| # | |
| # Changelog: | |
| # 2026.06.13-r1 | |
| # - Added strict Bash mode, lock, trap cleanup, root check, dependency check. | |
| # - Fixed unsafe unquoted variables and fragile MySQL version parsing. | |
| # - Added local ZIP reuse and multi-URL download fallback. | |
| # - Added ZIP integrity test before extraction. | |
| # - Added free-space check and minimal pre-upgrade backup. | |
| # - Prevented false success message if download, unzip, dos2unix, or upgrade fails. | |
| # - Kept vendor upgrade flow intact to avoid breaking working PNETLab behavior. | |
| # ============================================================================== | |
| set -Eeuo pipefail | |
| IFS=$'\n\t' | |
| umask 022 | |
| # ----------------------------- USER VARIABLES --------------------------------- | |
| NEW_PNETLAB_VERSION="${NEW_PNETLAB_VERSION:-5.3.11}" | |
| WORK_BASE="${WORK_BASE:-/root}" | |
| ZIP_FILE="${ZIP_FILE:-${WORK_BASE}/${NEW_PNETLAB_VERSION}.zip}" | |
| UPGRADE_DIR="${UPGRADE_DIR:-${WORK_BASE}/upgrade}" | |
| BACKUP_DIR="${BACKUP_DIR:-${WORK_BASE}/pnetlab-upgrade-backup}" | |
| LOG_FILE="${LOG_FILE:-${WORK_BASE}/pnetlab-upgrade-${NEW_PNETLAB_VERSION}-$(date +%Y%m%d-%H%M%S).log}" | |
| LOCK_DIR="${LOCK_DIR:-/run/lock/pnetlab-upgrade-${NEW_PNETLAB_VERSION}.lock}" | |
| MIN_FREE_MB="${MIN_FREE_MB:-2048}" | |
| AUTO_REBOOT="${AUTO_REBOOT:-1}" | |
| KEEP_ZIP="${KEEP_ZIP:-1}" | |
| AUTO_INSTALL_DEPS="${AUTO_INSTALL_DEPS:-1}" | |
| # Optional: put your own confirmed URL first. | |
| # Example: | |
| # export PNETLAB_UPGRADE_URL='https://example.tld/5.3.11.zip' | |
| PNETLAB_UPGRADE_URL="${PNETLAB_UPGRADE_URL:-}" | |
| # Best-effort download candidates. Official web release currently points users to | |
| # the 5.3.11 patch package; some mirrors may move or require browser/manual download. | |
| DOWNLOAD_URLS=( | |
| "${PNETLAB_UPGRADE_URL}" | |
| "https://unetlab.cloud/api/raw/?path=/UNETLAB%20I/upgrades_pnetlab/extras/from_any_to_${NEW_PNETLAB_VERSION}/${NEW_PNETLAB_VERSION}.zip" | |
| "https://labhub.eu.org/api/raw/?path=/UNETLAB%20I/upgrades_pnetlab/extras/from_any_to_${NEW_PNETLAB_VERSION}/${NEW_PNETLAB_VERSION}.zip" | |
| "https://legacy.labhub.eu.org/0%3A/UNETLAB%20I/upgrades_pnetlab/extras/from_any_to_${NEW_PNETLAB_VERSION}/${NEW_PNETLAB_VERSION}.zip" | |
| ) | |
| # ------------------------------- COLORS --------------------------------------- | |
| if [[ -t 1 ]]; then | |
| GREEN='\033[32m' | |
| YELLOW='\033[33m' | |
| RED='\033[31m' | |
| CYAN='\033[36m' | |
| NO_COLOR='\033[0m' | |
| else | |
| GREEN='' | |
| YELLOW='' | |
| RED='' | |
| CYAN='' | |
| NO_COLOR='' | |
| fi | |
| # ------------------------------ LOGGING --------------------------------------- | |
| log() { printf '%b[%s] %s%b\n' "${CYAN}" "$(date '+%F %T')" "$*" "${NO_COLOR}" | tee -a "${LOG_FILE}"; } | |
| ok() { printf '%b[OK] %s%b\n' "${GREEN}" "$*" "${NO_COLOR}" | tee -a "${LOG_FILE}"; } | |
| warn() { printf '%b[WARN] %s%b\n' "${YELLOW}" "$*" "${NO_COLOR}" | tee -a "${LOG_FILE}" >&2; } | |
| err() { printf '%b[ERROR] %s%b\n' "${RED}" "$*" "${NO_COLOR}" | tee -a "${LOG_FILE}" >&2; } | |
| die() { err "$*"; exit 1; } | |
| run() { | |
| log "RUN: $*" | |
| "$@" 2>&1 | tee -a "${LOG_FILE}" | |
| } | |
| cleanup() { | |
| local rc=$? | |
| if [[ -n "${APT_CONF_TMP:-}" && -f "${APT_CONF_TMP:-}" ]]; then | |
| rm -f -- "${APT_CONF_TMP}" | |
| fi | |
| if [[ -d "${LOCK_DIR}" ]]; then | |
| rmdir -- "${LOCK_DIR}" 2>/dev/null || true | |
| fi | |
| if (( rc != 0 )); then | |
| err "Upgrade stopped with exit code ${rc}. Check log: ${LOG_FILE}" | |
| fi | |
| exit "${rc}" | |
| } | |
| trap cleanup EXIT | |
| trap 'die "Interrupted by user or system signal."' INT TERM | |
| # ------------------------------ HELPERS --------------------------------------- | |
| have_cmd() { command -v "$1" >/dev/null 2>&1; } | |
| require_root() { | |
| [[ "${EUID}" -eq 0 ]] || die "Run this script as root." | |
| } | |
| create_lock() { | |
| mkdir -p "$(dirname "${LOCK_DIR}")" | |
| if ! mkdir "${LOCK_DIR}" 2>/dev/null; then | |
| die "Another PNETLab upgrade process seems to be running: ${LOCK_DIR}" | |
| fi | |
| } | |
| install_missing_deps() { | |
| local missing=() | |
| local required=(mysql unzip find xargs dos2unix awk sed df date tee) | |
| if ! have_cmd curl && ! have_cmd wget; then | |
| missing+=(curl) | |
| fi | |
| for cmd in "${required[@]}"; do | |
| have_cmd "${cmd}" || missing+=("${cmd}") | |
| done | |
| if (( ${#missing[@]} == 0 )); then | |
| ok "All required tools are available." | |
| return 0 | |
| fi | |
| warn "Missing tools: ${missing[*]}" | |
| if [[ "${AUTO_INSTALL_DEPS}" != "1" ]]; then | |
| die "Install missing tools first or run with AUTO_INSTALL_DEPS=1." | |
| fi | |
| have_cmd apt-get || die "apt-get not found; cannot auto-install dependencies." | |
| export DEBIAN_FRONTEND=noninteractive | |
| run apt-get update | |
| run apt-get install -y --no-install-recommends curl wget unzip dos2unix ca-certificates gzip tar | |
| ok "Dependency installation completed." | |
| } | |
| check_free_space() { | |
| local target_dir free_mb | |
| target_dir="${WORK_BASE}" | |
| mkdir -p "${target_dir}" | |
| free_mb=$(df -Pm "${target_dir}" | awk 'NR==2 {print $4}') | |
| [[ "${free_mb}" =~ ^[0-9]+$ ]] || die "Unable to read free disk space for ${target_dir}." | |
| if (( free_mb < MIN_FREE_MB )); then | |
| die "Free space on ${target_dir} is ${free_mb} MB; minimum required is ${MIN_FREE_MB} MB." | |
| fi | |
| ok "Free space on ${target_dir}: ${free_mb} MB." | |
| } | |
| mysql_query() { | |
| mysql --batch --skip-column-names -uroot -ppnetlab -D pnetlab_db -e "$1" 2>/dev/null | |
| } | |
| get_pnetlab_version() { | |
| local version | |
| version=$(mysql_query "SELECT control_value FROM control WHERE control_value REGEXP '^[0-9]+(\\.[0-9]+){1,3}$' LIMIT 1;" | head -n1 | tr -d '[:space:]' || true) | |
| if [[ -n "${version}" ]]; then | |
| printf '%s\n' "${version}" | |
| return 0 | |
| fi | |
| if have_cmd dpkg-query; then | |
| version=$(dpkg-query -W -f='${Version}\n' pnetlab 2>/dev/null | head -n1 | sed 's/^[^0-9]*//; s/[^0-9.].*$//' || true) | |
| if [[ -n "${version}" ]]; then | |
| printf '%s\n' "${version}" | |
| return 0 | |
| fi | |
| fi | |
| return 1 | |
| } | |
| validate_version_state() { | |
| local current_version | |
| current_version="$(get_pnetlab_version || true)" | |
| if [[ -z "${current_version}" ]]; then | |
| die "Unable to detect current PNETLab version from pnetlab_db or dpkg." | |
| fi | |
| log "Detected PNETLab version: ${current_version}" | |
| if [[ "${current_version}" == "${NEW_PNETLAB_VERSION}" ]]; then | |
| ok "PNETLab is already upgraded to v${NEW_PNETLAB_VERSION}. Upgrade is skipped." | |
| exit 0 | |
| fi | |
| if [[ "${current_version}" == 6.* ]]; then | |
| die "Current version is ${current_version}. Downgrade/upgrade from PNETLab 6.x to ${NEW_PNETLAB_VERSION} is not supported by this script." | |
| fi | |
| if [[ ! "${current_version}" =~ ^[0-9]+(\.[0-9]+){1,3}$ ]]; then | |
| die "Detected version '${current_version}' is not valid. Aborting for safety." | |
| fi | |
| } | |
| make_backup() { | |
| local stamp dest | |
| stamp="$(date +%Y%m%d-%H%M%S)" | |
| dest="${BACKUP_DIR}/${stamp}" | |
| mkdir -p "${dest}" | |
| log "Creating pre-upgrade backup in ${dest}" | |
| if have_cmd mysqldump; then | |
| if mysqldump -uroot -ppnetlab --single-transaction --quick pnetlab_db 2>>"${LOG_FILE}" | gzip -9 > "${dest}/pnetlab_db.sql.gz"; then | |
| ok "Database backup saved: ${dest}/pnetlab_db.sql.gz" | |
| else | |
| warn "Database backup failed. Continuing is unsafe." | |
| die "mysqldump failed; fix MySQL access before upgrading." | |
| fi | |
| else | |
| warn "mysqldump not found; database backup skipped." | |
| fi | |
| if [[ -d /opt/unetlab/html/templates ]]; then | |
| tar -czf "${dest}/templates.tgz" -C /opt/unetlab/html templates 2>>"${LOG_FILE}" || warn "Template backup failed." | |
| fi | |
| if [[ -d /opt/unetlab/data/Logs ]]; then | |
| tar -czf "${dest}/logs-before-upgrade.tgz" -C /opt/unetlab/data Logs 2>>"${LOG_FILE}" || warn "Logs backup failed." | |
| fi | |
| ok "Pre-upgrade backup stage completed." | |
| } | |
| is_valid_zip() { | |
| local zip="$1" | |
| [[ -s "${zip}" ]] || return 1 | |
| unzip -tqq "${zip}" >/dev/null 2>&1 | |
| } | |
| download_with_curl_or_wget() { | |
| local url="$1" | |
| local output="$2" | |
| local tmp="${output}.part" | |
| rm -f -- "${tmp}" | |
| if have_cmd curl; then | |
| curl -fL --connect-timeout 20 --retry 3 --retry-delay 3 --output "${tmp}" "${url}" >>"${LOG_FILE}" 2>&1 | |
| else | |
| wget -q --show-progress --tries=3 --timeout=20 -O "${tmp}" "${url}" >>"${LOG_FILE}" 2>&1 | |
| fi | |
| if is_valid_zip "${tmp}"; then | |
| mv -f -- "${tmp}" "${output}" | |
| return 0 | |
| fi | |
| rm -f -- "${tmp}" | |
| return 1 | |
| } | |
| obtain_upgrade_zip() { | |
| local url | |
| if is_valid_zip "${ZIP_FILE}"; then | |
| ok "Using existing valid ZIP: ${ZIP_FILE}" | |
| return 0 | |
| fi | |
| rm -f -- "${ZIP_FILE}" | |
| for url in "${DOWNLOAD_URLS[@]}"; do | |
| [[ -n "${url}" ]] || continue | |
| log "Downloading ${NEW_PNETLAB_VERSION} upgrade ZIP from: ${url}" | |
| if download_with_curl_or_wget "${url}" "${ZIP_FILE}"; then | |
| ok "Downloaded and validated: ${ZIP_FILE}" | |
| return 0 | |
| fi | |
| warn "Download failed or file is not a valid ZIP: ${url}" | |
| done | |
| die "Unable to download a valid ${NEW_PNETLAB_VERSION}.zip. Download it manually from PNETLab release page, upload it to ${ZIP_FILE}, then rerun this script." | |
| } | |
| safe_remove_upgrade_dir() { | |
| if [[ "${UPGRADE_DIR}" != "${WORK_BASE}/upgrade" && "${UPGRADE_DIR}" != /root/upgrade ]]; then | |
| die "Refusing to remove suspicious UPGRADE_DIR: ${UPGRADE_DIR}" | |
| fi | |
| rm -rf -- "${UPGRADE_DIR}" | |
| } | |
| extract_upgrade_zip() { | |
| log "Preparing upgrade directory: ${UPGRADE_DIR}" | |
| safe_remove_upgrade_dir | |
| mkdir -p "${UPGRADE_DIR}" | |
| log "Unzipping ${ZIP_FILE} to ${UPGRADE_DIR}" | |
| unzip -q "${ZIP_FILE}" -d "${UPGRADE_DIR}" | |
| # Keep vendor-compatible permissions. PNETLab's official manual upgrade step uses chmod 755 -R. | |
| chmod 755 -R "${UPGRADE_DIR}" | |
| log "Converting CRLF line endings under ${UPGRADE_DIR}" | |
| find "${UPGRADE_DIR}" -type f -print0 | xargs -0 dos2unix >/dev/null 2>&1 | |
| if [[ ! -x "${UPGRADE_DIR}/upgrade" ]]; then | |
| die "Upgrade entrypoint not found or not executable: ${UPGRADE_DIR}/upgrade" | |
| fi | |
| ok "Upgrade package extracted and prepared." | |
| } | |
| run_vendor_upgrade() { | |
| APT_CONF_TMP="$(mktemp /tmp/pnetlab-apt-conf.XXXXXX)" | |
| printf '%s\n' 'APT::Get::Assume-Yes "true";' > "${APT_CONF_TMP}" | |
| export APT_CONFIG="${APT_CONF_TMP}" | |
| export DEBIAN_FRONTEND=noninteractive | |
| log "Starting vendor upgrade script: ${UPGRADE_DIR}/upgrade" | |
| (cd "${WORK_BASE}" && "${UPGRADE_DIR}/upgrade") 2>&1 | tee -a "${LOG_FILE}" | |
| ok "Vendor upgrade command completed." | |
| } | |
| verify_after_upgrade() { | |
| local version_after | |
| version_after="$(get_pnetlab_version || true)" | |
| if [[ "${version_after}" == "${NEW_PNETLAB_VERSION}" ]]; then | |
| ok "Verified PNETLab version after upgrade: ${version_after}" | |
| return 0 | |
| fi | |
| warn "Upgrade command completed, but detected version is '${version_after:-unknown}', not '${NEW_PNETLAB_VERSION}'." | |
| warn "Check web GUI System > Version and log file: ${LOG_FILE}" | |
| } | |
| post_cleanup() { | |
| log "Cleaning temporary upgrade directory." | |
| safe_remove_upgrade_dir | |
| if [[ "${KEEP_ZIP}" != "1" ]]; then | |
| rm -f -- "${ZIP_FILE}" | |
| ok "Removed ZIP file: ${ZIP_FILE}" | |
| else | |
| ok "Keeping ZIP file for reuse: ${ZIP_FILE}" | |
| fi | |
| } | |
| reboot_if_enabled() { | |
| if [[ "${AUTO_REBOOT}" == "1" ]]; then | |
| ok "Upgrade finished successfully. Rebooting now." | |
| reboot | |
| else | |
| ok "Upgrade finished successfully. AUTO_REBOOT=0, reboot skipped. Run 'reboot' manually." | |
| fi | |
| } | |
| main() { | |
| touch "${LOG_FILE}" || die "Cannot write log file: ${LOG_FILE}" | |
| log "PNETLab upgrade helper started. Target version: ${NEW_PNETLAB_VERSION}" | |
| require_root | |
| create_lock | |
| install_missing_deps | |
| check_free_space | |
| validate_version_state | |
| make_backup | |
| obtain_upgrade_zip | |
| extract_upgrade_zip | |
| run_vendor_upgrade | |
| verify_after_upgrade | |
| post_cleanup | |
| reboot_if_enabled | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment