Skip to content

Instantly share code, notes, and snippets.

@alsyundawy
Forked from pertsevds/upgrade.sh
Last active June 13, 2026 16:28
Show Gist options
  • Select an option

  • Save alsyundawy/d6e8e62e9409f2b45ecbe5e230f8f52d to your computer and use it in GitHub Desktop.

Select an option

Save alsyundawy/d6e8e62e9409f2b45ecbe5e230f8f52d to your computer and use it in GitHub Desktop.
Upgrade Pnetlab from_any_to_5.3.11 with reboot without confirmation
#!/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