#!/usr/bin/env bash
#
# NetScaler Firmware Upgrade/Downgrade Script
#
# Copyright 2025 Citrix Systems, Inc. All rights reserved.
# This software and documentation contain valuable trade
# secrets and proprietary property belonging to Citrix Systems, Inc.
# None of this software and documentation may be copied,
# duplicated or disclosed without the express
# written permission of Citrix Systems, Inc.
#

## INSTALLER FUNCTIONS:
usage() {
	cat <<EOF

Usage: ./$installer_basename [options]

Generic options:
 -g: non-interactive GUI/API driven installation. same as -Y but doesn't
     include automatic reboot.
 -h: print this help message
 -n: don't reboot automatically, requires manual reboot from user.
     (supersedes option 'y')
 -y: reboot automatically as part of installation steps when required.
 -Y: answer 'Yes' to everything. it includes automatic reboot (option 'y')
     and skips several pre-checks.

Advanced options:
 -a: add netscaler config for new version
 -S: ignore supported platform checks
 -v: enable verbose/debug logging
 -Z: launch bash prompt on failure for debug purpose
EOF
}

init_installer_info() {
	MAX_ATTEMPTS=5
	PIDFILE="/run/installns.pid"
	RESUME_FILENAME=".ns-installer.resume"

	DEFAULT_RESUME_FILEPATH="/var/nsinstall/$RESUME_FILENAME"
	ALTERNATE_RESUME_FILEPATH="/flash$DEFAULT_RESUME_FILEPATH"
	RESUME_FILES=("$DEFAULT_RESUME_FILEPATH" "$ALTERNATE_RESUME_FILEPATH")

	TRANSIT_BUILD_REL_A=14
	TRANSIT_BUILD_REL_B=1
	TRANSIT_BUILD_VER_A=56

	# pre-init runtime variables
	installer=$(realpath "$0")
	installer_basename=$(basename "$installer")
	install_dirpath=$(dirname "$installer")
	install_dirpath_original="$install_dirpath"
	install_start_timestamp=$(date '+%F %T')

	# local resume file is useful to skip switch to legacy when in l2f mode
	local_resume_filepath="$install_dirpath/$RESUME_FILENAME"
	RESUME_FILES+=("$local_resume_filepath")

	uname_out=$(uname -a)
	[[ "$uname_out" == "FreeBSD "* ]] && running_freebsd_ns=1 || running_freebsd_ns=0
	grep -q "^ID=netscaler$" /etc/os-release 2>/dev/null && running_linux_ns=1 || running_linux_ns=0
	grep -q "^ID=ns-installer$" /etc/os-release 2>/dev/null && running_linux_ns_installer=1 || running_linux_ns_installer=0

	if [[ $running_freebsd_ns == 1 ]]; then
		PIDFILE="/var/run/installns.pid"
	fi

	if [[ "$NSVARIANT" == "NSLINUX" ]]; then
		installer_files_path="$install_dirpath/ns-installer.files"
		installer_files_list=$(cat "$installer_files_path" | awk '{print $2}')
		bzimage_name=$(echo "$installer_files_list" | grep "bzImage-" | grep -v ".signed")
		initrd_name=$(echo "$installer_files_list" | grep "initrd-$NSVERSION.cpio.gz" | grep -v ".signed")
		rootfs_name=$(echo "$installer_files_list" | grep "rootfs-$NSVERSION.tar.gz" | grep -v ".signed")
		rsapubkey_name=$(echo "$installer_files_list" | grep "rsakey-${NSVERSION}_pub.pem")

		if [[ $running_freebsd_ns == 1 ]]; then
			linux_version=$(file $bzimage_name | \
				awk '{ for(i=1; i<=NF; i++) { if($i == "version") {print "(Linux v"$(i+1)")"; break; } } }')
		elif [[ $running_linux_ns_installer == 1 ]]; then
			linux_version=$(echo "$uname_out" | awk '{print "(Linux v"$3")"}')
		elif [[ $running_linux_ns == 1 ]]; then
			linux_version=$(file $bzimage_name | \
				awk '{ for(i=1; i<=NF; i++) { if($i == "version") {print "(Linux v"$(i+1)")"; break; } } }')
		fi
	fi

	_fetch_resume_file
}

set_installer_singleton() {
	if [ -f "$PIDFILE" ]; then
		other_pid=$(cat "$PIDFILE")
		if [ "$(ps -p $other_pid -o comm=)" == "$installer_basename" ]; then
			error "Another instance of the installer is running. Exiting."
			exit 203
		fi
	fi
	echo $$ > "$PIDFILE"
}

load_options() {
	# Load the options from the command line
	OPTIONS_ALLOWED="agGhnNSvyYZ"
	OPTIONS_ALLOWED+="AcDefFLMpR6" # backward compatibility
	OPTIONS_GIVEN=
	while getopts "$OPTIONS_ALLOWED" opt; do
		case $opt in
			a) OPTION_NSCONF_ADD=true ;;
			g)
				OPTION_GUI_FLAG=true
				OPTION_SKIP_NSCONF_CHECK=true
				OPTION_ASSUME_YES=true
				;;
			G) OPTION_NSCONF_NOCURSES=true ;;
			h)
				info "NetScaler Firmware Upgrade Script (version $NSVERSION)"
				usage
				nsexit
				;;
			n)
				OPTION_NO_REBOOT=true
				;;
			N) OPTION_SKIP_NSCONF_CHECK=true ;;
			S) OPTION_IGNORE_PLATFORM_CHECKS=true ;;
			v) OPTION_VERBOSE=true ;;
			y)
				if [[ $OPTION_NO_REBOOT == true ]]; then
					error "option 'y' and 'n' are not supported together"
				fi
				OPTION_FORCE_REBOOT=true
				;;
			Y)
				OPTION_SKIP_NSCONF_CHECK=true
				OPTION_ASSUME_YES=true
				OPTION_FORCE_REBOOT=true
				;;
			Z) OPTION_BASH_ON_ERROR=true ;;
			A | c | D | e | f | F | L | M | p | R | 6 )
				# backward compatibility: additional options handled by legacy installer but are
				# obsolete for this framework. do not fail installer, just ignore.
				;;
			\?)
				warn "Invalid option provided, please refer usage:"
				usage
				nsexit 204
				;;
		esac
		OPTIONS_GIVEN="$OPTIONS_GIVEN$opt"
	done
}

_switch_installer_as_per_resume() {
	local resume_installer=$(realpath "$install_dirpath/$installer_basename")
	if [[ -f "$resume_installer" ]] && [[ "$resume_installer" != "$installer" ]]; then
		info "restarting installer script"
		debug "running remote installer script, switch to $install_dirpath"
		_disable_signal_handling
		local options=
		if [[ -n "$OPTIONS_GIVEN" ]]; then
			options="-$OPTIONS_GIVEN"
		fi
		disable_logging
		cd "$install_dirpath" && ./$installer_basename $options
		exit $?
	fi
}

nsexit() {
	_disable_signal_handling
	if [ -n "$1" ]; then
		[ $1 -ne 0 ] && _clean_resume_file
		exit $1
	fi
	exit
}

init_signal_handling() {
	trap _th_interrupt INT TERM HUP
	trap _th_exit QUIT EXIT
}

_disable_signal_handling() {
	trap - INT TERM HUP QUIT EXIT
}

_fetch_resume_file() {
	for r_f in "${RESUME_FILES[@]}"; do
		if [ -f "$r_f" ]; then
			if [ -n "$resume_file" ] && [[ "$resume_file" != "$r_f" ]]; then
				debug "found duplicate of '$resume_file': '$r_f', clean and continue"
				rm -f "$r_f"
				continue
			fi
			source "$r_f"
			if [[ $? != 0 ]]; then
				debug "failed to load '$r_f', clean and continue."
				rm -f '$r_f'
				continue
			fi
			resume_file="$r_f"
			break
		fi
	done
}

_write_resume_file() {
	if [ -z "$resume_file" ]; then
		for r_f in "${RESUME_FILES[@]}"; do
			r_f_d=$(dirname "$r_f")
			if [ -d "$r_f_d" ]; then
				run touch "$r_f"
				if [[ $? == 0 ]]; then
					resume_file="$r_f"
					break
				fi
			fi
		done
	fi

	if [ -n "$1" ] && [[ "$1" != $resume_file ]]; then
		mkdir -p $(dirname "$1")
		debug "moving resume file to '$1'"
		run mv "$resume_file" "$1"
		resume_file="$1"
	fi

	if [ ! -f "$resume_file" ] ; then
		warn "failed to create '$resume_file'"
		return 1
	fi

	debug "write resume file: '$resume_file'"
	echo "install_mode=$install_mode" > "$resume_file"
	echo "install_dirpath=\"$install_dirpath\"" >> "$resume_file"
	echo "install_dirpath_original=\"$install_dirpath_original\"" >> "$resume_file"
	echo "running_phase=$running_phase" >> "$resume_file"
	echo "install_start_timestamp=\"$install_start_timestamp\"" >> "$resume_file"
	echo "phase_attempts=$phase_attempts" >> "$resume_file"
	if [[ $OPTION_VERBOSE == true ]]; then
		echo "OPTION_VERBOSE=true" >> "$resume_file"
	fi
	if [[ $OPTION_BASH_ON_ERROR == true ]]; then
		echo "OPTION_BASH_ON_ERROR=true" >> "$resume_file"
	fi
	_log2file "$(cat "$resume_file")"
}

_clean_resume_file() {
	if [ -n "$resume_file" ]; then
		run cat "$resume_file"
		rm -f "$resume_file"
	fi
	resume_file=
	for r_f in "${RESUME_FILES[@]}"; do
		if [ -f "$r_f" ]; then
			rm -f "$r_f"
		fi
	done
}

init_system_info() {
	# Populate bare minimal system information required to start installer
	_switch_installer_as_per_resume
	_populate_system_info
	if [[ $running_freebsd_ns == 1 ]]; then
		_populate_system_info_freebsd
	elif [[ $running_linux_ns == 1 ]]; then
		_populate_system_info_linux
	elif [[ $running_linux_ns_installer == 1 ]]; then
		_populate_system_info_linux_installer
	else
		error "running on an unknown OS, not supported"
	fi
}

_detect_install_mode() {
	if [[ $running_freebsd_ns == 1 ]]; then
		if [[ $NSVARIANT == "NSLINUX" ]]; then
			install_mode="f2l"
		else
			error "unsupported installer variant for vpx-legacy: $NSVARIANT"
		fi
	elif [[ $running_linux_ns == 1 ]]; then
		if [[ $NSVARIANT == "v" ]]; then
			install_mode="l2f"
		elif [[ $NSVARIANT == "NSLINUX" ]]; then
			install_mode="l2l"
		else
			error "unsupported installer variant for vpx-linux: $NSVARIANT"
		fi
	elif [[ $running_linux_ns_installer == 1 ]]; then
		error "starting installer from vpx-linux-installer is not supported"
	else
		error "running on an unknown OS, not supported"
	fi
}

_print_installer_start() {
	if [[ $NSVARIANT == "NSLINUX" ]]; then
		info_kernel="$bzimage_name $linux_version"
	elif [[ $NSVARIANT == "v" ]]; then
		kernel_name=ns-$NSVERSION.gz
		freebsd_version="(FreeBSD)"
		info_kernel="$kernel_name $freebsd_version"
	fi
	cat <<EOF
==== NETSCALER FIRMWARE UPGRADE SCRIPT ========================================

BEGIN TIME        : $install_start_timestamp
NEW VERSION       : $NSVERSION
NEW KERNEL        : $info_kernel
INSTALLER OPTIONS : [$OPTIONS_GIVEN]
EOF
	if [ -z "$info_kernel" ]; then
		error "Failed to get new kernel info, please check"
	fi
}

switch2legacy_installer() {
	if [ ! -f installns_legacy ]; then
		error "legacy installer script missing"
	fi
	_disable_signal_handling
	NSVERSION=$NSVERSION NSVARIANT=$NSVARIANT CLVERSION=$CLVERSION ./installns_legacy "$@"
}

_find_freebsd_secdisk_and_format() {
	ALL_DISKS=$(lsblk -b -o NAME,SIZE,TYPE | grep "disk" | awk '$2 >= 8589934592 {print $1}')
	set -- $ALL_DISKS
	shift
	info "secondary disks attached to VM will be formatted"
	for disk_name in $@; do
		debug "secondary disk ($disk_name) found, format and adapt to linux filesystem."
		dd if=/dev/zero of="/dev/${disk_name}" bs=1M count=1
	done
}

pre_upgrade() {
	_print_installer_start
	_check_installer
	_check_installer_compatibility
}

############################################################################################
# NOTE: This is a stopgap solution for any modifications to /var during upgrades
#
# TARGET: Move all static files/binaries to proper BLX RPM packages when possible and
#         make blx service files handle any pre-upgrade operations
#
# TODO: Eventually eliminate this function
#############################################################################################
install_upgrade_prerequisites() {
	# misc operations done prior to upgrading the appliance.
	if [[ "$install_mode" == "f2l" ]]; then
		info "Install operations prior to upgrade (f2l)"
	elif [[ "$install_mode" == "l2l" ]]; then
		info "Install operations prior to upgrade (l2l)"
		install_safenet
	elif [[ "$install_mode" == "l2f" ]]; then
		info "Install operations prior to upgrade (l2f)"
	fi
}

run_upgrade() {
	if [[ -z "$install_mode" ]]; then
		_check_ns_system
		_detect_install_mode
		_print_banner #TBD: Update banner for l2l or l2f
		_print_warning
		_write_resume_file
	else
		info_step "Resume existing upgrade/installation"
		info "Done"
		debug "loaded resume file: '$resume_file'"
	fi

	_populate_system_info
	if [[ "$install_mode" == "f2l" ]]; then
		debug "f2l mode running"
		if [[ $running_freebsd_ns == 1 ]]; then
			if [[ $running_phase == "" ]]; then
				_collect_techsupport_bundle
				running_phase=1
				_write_resume_file
			fi
			if [[ $running_phase == 1 ]]; then
				install_upgrade_prerequisites
				_f2l_init_info_p1
				_f2l_flash_clean_obsolete_files
				_f2l_check_flash_space
				_f2l_swapoff_next_flash
				_f2l_flash_extend
				_f2l_var2flash_backup
				running_phase=2
				_write_resume_file "$ALTERNATE_RESUME_FILEPATH"
			fi
			if [[ $running_phase == 2 ]] || [[ $running_phase == 3 ]]; then
				_switch_logging_to_path "/flash/$install_dirpath/$LOGFILENAME"
			fi
			if [[ $running_phase == 2 ]]; then
				_f2l_grub_install
				install_dirpath="/flash/$install_dirpath"
				running_phase=3
				_write_resume_file "$ALTERNATE_RESUME_FILEPATH"
			fi
			if [[ $running_phase == 3 ]]; then
				_fix_downgrade_issues
				cp -f "$resume_file" "$INIT_RESUME_FLAG"
				_reboot
			fi
		elif [[ $running_linux_ns_installer == 1 ]]; then
			if [[ ${running_phase:-0} -lt 3 ]]; then
				warn "Incorrect phase for ns-installer, attempting resume"
				running_phase=3
			fi
			if [[ $running_phase == 3 ]]; then
				_f2l_remove_freebsd_var_partition
				_f2l_shrink_freebsd_slice
				_create_linux_post_extended_partition
				running_phase=4
				_write_resume_file "$ALTERNATE_RESUME_FILEPATH"
			fi
			if [[ $running_phase == 4 ]]; then
				_f2l_var_restore
				_f2l_flash_backup
				install_dirpath="$install_dirpath_original"
				_install_grub_to_var
				_switch_logging_to_path "$install_dirpath/$LOGFILENAME"
				running_phase=5
				_write_resume_file "$DEFAULT_RESUME_FILEPATH"
				_switch_installer_as_per_resume
			fi
			if [[ $running_phase == 5 ]]; then
				cp -f "$resume_file" "$INIT_VAR_RESUME_FLAG"
				_create_linux_pre_extended_partition
				_install_linux_artifacts
				_install_grub_to_boot
				_f2l_flash_restore
				rm -f "$INIT_VAR_RESUME_FLAG"
				running_phase=6
				_write_resume_file
			fi
			if [[ $running_phase == 6 ]]; then
				rm -f "$INIT_RESUME_FLAG"
				_find_freebsd_secdisk_and_format
				_reboot
			fi
		elif [[ $running_linux_ns == 1 ]]; then
			if [[ $running_phase == 6 ]]; then
				install_mode="post_f2l"
				running_phase=
				_write_resume_file
			fi
		else
			error "running on an unknown OS, not supported"
		fi
	elif [[ "$install_mode" == "l2f" ]]; then
		debug "l2f mode running"
		if [[ $running_linux_ns == 1 ]]; then
			if [[ $running_phase == "" ]]; then
				# Skip generating new techbudle while downgrade
				#_collect_techsupport_bundle
				_l2f_init_info_p1
				_delete_older_linux_images
				_l2f_check_flash_space
				running_phase=1
				_write_resume_file
			fi
			if [[ $running_phase == 1 ]]; then
				install_upgrade_prerequisites
				_l2f_init_info_p1
				_l2f_swapoff_next_flash
				_l2f_flash_extend
				_l2f_installer_backup_to_flash
				install_dirpath="/flash$install_dirpath"
				_switch_logging_to_path "$install_dirpath/$LOGFILENAME"
				running_phase=2
				_write_resume_file "$ALTERNATE_RESUME_FILEPATH"
			fi
			if [[ $running_phase == 2 ]]; then
				_fix_downgrade_issues
				cp -f "$resume_file" "$INIT_RESUME_FLAG"
				_reboot
			fi
		elif [[ $running_linux_ns_installer == 1 ]]; then
			if [[ $running_phase == 2 ]]; then
				_switch_installer_as_per_resume
				_l2f_var_clean_before_shrink
				_l2f_shrink_var_n_create_tmp_partition
				running_phase=3
				_write_resume_file
			fi
			if [[ $running_phase == 3 ]]; then
				_mount_l2f_backup_partitions
				_l2f_backup_to_tmp_partition
				running_phase=4
				_write_resume_file
			fi
			if [[ $running_phase == 4 ]]; then
				_mount_l2f_backup_partitions
				_l2f_shrink_extended_partition
				_l2f_backup_to_tmp_installer_files
				running_phase=5
				_write_resume_file
			fi
			if [[ $running_phase == 5 ]]; then
				_mount_l2f_backup_partitions
				_l2f_install_grub_to_backup2
				install_dirpath="$backup_install_dirpath"
				_write_resume_file "$install_dirpath/$RESUME_FILENAME"
				_reboot
			fi
		elif [[ $running_freebsd_ns == 1 ]]; then
			if [[ $running_phase == 5 ]]; then
				_l2f_restore_freebsd_partitions
				_l2f_install_freebsd_netscaler
				running_phase=6
				_write_resume_file
			fi
			if [[ $running_phase == 6 ]]; then
				_l2f_remove_backup_partitions
				install_mode="post_l2f"
				running_phase=
				_write_resume_file
			fi
		fi
	elif [[ "$install_mode" == "l2l" ]]; then
		debug "l2l mode running"
		if [[ $running_linux_ns == 1 ]]; then
			if [[ $running_phase == "" ]]; then
				install_upgrade_prerequisites
				_delete_older_linux_images
				_install_linux_artifacts
				running_phase=1
				_write_resume_file
			fi
			if [[ $running_phase == 1 ]]; then
				_fix_downgrade_issues
				[[ -f "$INIT_NSINSTALL_FLAG" ]] && _reboot
				install_mode="post_upgrade"
				running_phase=
				_write_resume_file
			fi
		fi
	else
		error "Unsupported netscaler install/upgrade mode"
	fi
}

post_upgrade() {
	# misc operations done after upgrade of netscaler booted with upgraded version.
	if [[ "$install_mode" == "post_f2l" ]]; then
		# post f2l upgrade operations
		info "NetScaler upgrade successful (F2L)"
	elif [[ "$install_mode" == "post_l2f" ]]; then
		# post l2f upgrade operations
		_l2f_restore_from_joined_archive
		info "NetScaler upgrade successful (L2F)"
	elif [[ "$install_mode" == "post_upgrade" ]]; then
		# post l2l upgrade operations
		info "NetScaler upgrade successful"
	fi

	_clean_resume_file
}

## NETSCALER PRE-CHECKS
SUPPORTED_SYSID=(
		4500[01279][01]
		450091
	)

precheck_fmsgs=()

_check_downgrade_prerequisite() {
	dg_pr_dir="/netscaler/downgrade_prerequisite"
	dg_pr_main="$dg_pr_dir/main"

	if [ -f "$dg_pr_main" ]; then
		info_step "Check version compatibility"
		chmod +x "$dg_pr_main"
		out=$(NSVERSION=$NSVERSION NSVARIANT=$NSVARIANT CLVERSION=$CLVERSION "$dg_pr_main" 2>&1)
		rcode=$?
		debug "script output (code=$rcode): $out"

		# refer downgrade_prerequisite/README.NETSCALER for expected return codes
		if [ $rcode -eq 0 ]; then
			info "Done"
		elif [ $((rcode & 0xf0)) -eq $((0x10)) ]; then
			info "Done with warnings"
			info "Downgrade prerequisite output: $out"
			if [ $((rcode & 0x0f)) -ne 0 ]; then
				# we need to confirm for downgrade
				warn "Downgrade prerequisite passed with warnings, please review warnings"
				if [[ $OPTION_ASSUME_YES == true ]]; then
					echo "[Option 'Y' set, Continuing with downgrade ....]"
				else
					_confirmation_prompt "Do you want to continue with downgrade" "n" 213
				fi
			fi
		elif [ $((rcode & 0x0f)) -ne 0 ]; then
			info "Failed"
			info "version compatibility prerequisite output: $out"
			error "version compatibility prerequisite check failed (code=$rcode)"
		else
			info "Unknown"
			info "version compatibility prerequisite output: $out"
			warn "version compatibility prerequisite check gave unknown error code: $rcode"
			warn "Please review output before proceeding with installation"
			if [[ $OPTION_ASSUME_YES == true ]]; then
				echo "[Option 'Y' set, Continuing with installation ....]"
			else
				_confirmation_prompt "Do you want to continue with installation" "n" 214
			fi
		fi
	fi
}

_check_freebsd_sysid() {
	info_step "Check platform compatibility"
	if [[ $OPTION_IGNORE_PLATFORM_CHECKS == true ]]; then
		info "Skipped"
		return
	fi

	sysctl_ns__sysid=$(sysctl -n netscaler.sysid)
	for ssid in "${SUPPORTED_SYSID[@]}"; do
		if [[ $sysctl_ns__sysid =~ $ssid ]]; then
			info "Done"
			debug "netscaler sysid '$sysctl_ns__sysid' is supported by installer"
			return
		fi
	done
	info "Failed"
	precheck_fmsgs+=("Unsupported NetScaler system id: $sysctl_ns__sysid")
}

_check_freebsd_vpx() {
	sysctl_ns__sdxvpx=$(sysctl -n netscaler.sdxvpx)
	sysctl_ns__hypervised_ns=$(sysctl -n netscaler.hypervised_ns)
	sysctl_ns__vpx_on_cloud=$(sysctl -n netscaler.vpx_on_cloud)

	if [[ $sysctl_ns__sdxvpx == 1 ]]; then
		precheck_fmsgs+=("VPX on SDX is not supported yet")
	elif [[ $sysctl_ns__hypervised_ns == 1 ]]; then
		debug "Hypervised VPX is supported by installer"
	elif [[ $sysctl_ns__vpx_on_cloud == 1 ]]; then
		debug "VPX on Cloud is supported by installer"
	else
		return
	fi

	info_step "Check VPX requirements"
	rc="Done"
	sysctl_hw__ncpu=$(sysctl -n hw.ncpu)
	if [[ $sysctl_hw__ncpu -lt 2 ]]; then
		precheck_fmsgs+=("VPX requires minimum 2 cpus to start")
		rc="Failed"
	fi

	sysctl_hw__realmem=$(sysctl -n hw.realmem)
	if [[ $sysctl_hw__realmem -lt $(( 2*(10**9) )) ]]; then
		precheck_fmsgs+=("VPX requires minimum 2 Gigabytes memory to start")
		rc="Failed"
	fi
	info $rc
}

_check_linux_sysid() {
	info_step "Check platform compatibility"
	if [[ $OPTION_IGNORE_PLATFORM_CHECKS == true ]]; then
		info "Skipped"
		return
	fi

	for ssid in "${SUPPORTED_SYSID[@]}"; do
		if [[ $NS_LINUX_SYSID =~ $ssid ]]; then
			info "Done"
			debug "netscaler sysid '$NS_LINUX_SYSID' is supported by installer"
			return
		fi
	done
	info "Failed"
	precheck_fmsgs+=("Unsupported NetScaler system id: $NS_LINUX_SYSID")
}

_check_linux_vpx() {
	info_step "Check VPX requirements"
	rc="Done"
	cpu_count=$(cat /proc/cpuinfo | grep -c "^processor\s:")
	if [[ $cpu_count -lt 2 ]]; then
		precheck_fmsgs+=("VPX requires minimum 2 cpus to start")
		rc="Failed"
	fi

	memTotal_k=$(cat /proc/meminfo | grep "^MemTotal:" | awk '{print $2}')
	if [[ $memTotal_k -lt $(( 2*(10**6) )) ]]; then
		precheck_fmsgs+=("VPX requires minimum 2 Gigabytes memory to start")
		rc="Failed"
	fi
	info $rc
}

_check_nsconf() {
	info_step "Check saved ns config"
	if [[ ! -f "$install_dirpath/nsconfig-$NSVERSION" ]]; then
		warn "nsconfig binary missing from instaler files, nsconf check skipped"
		return
	fi
	if [[ $OPTION_SKIP_NSCONF_CHECK == true ]]; then
		info "Skipped"
		return
	fi

	local rc="Done"
	local nsconfig_cmd_params="check wait"
	if [[ $OPTION_NSCONF_NOCURSES == true ]]; then
		nsconfig_cmd_params+=",nocurses"
	fi
	if [[ $OPTION_NSCONF_ADD == true ]]; then
		nsconfig_cmd_params+=" advance"
	fi

	chmod +x "$install_dirpath/nsconfig-$NSVERSION"
	$install_dirpath/nsconfig-$NSVERSION $nsconfig_cmd_params

	if [[ $? -eq 2 ]]; then
		precheck_fmsgs+=("Corresponding ns.conf either not found or corrupt."\
			"if you wish to view available ns.conf files use the -a option.")
		rc="Failed"
	fi
	info $rc
}

_check_var_free_space() {
	info_step "Check free space on /var"
	rc="Done"
	var_free_m=$(df -m /var | grep /var | awk '{print $4}')
	if [[ $var_free_m -lt 4096 ]]; then
		precheck_fmsgs+=("VPX requires minimum 4GiB of free space on /var to start upgrade")
		rc="Failed"
	fi
	info $rc
}

_check_ns_system() {
	# Pre-Upgrade/Installer Operation checks for running NetScaler
	if [[ "$install_dirpath" != "/var/"* ]]; then
		warn "installer must be started from /var/** path"
		error "please move upgrade files to /var partition or its subdirectories and restart upgrade script"
	fi

	_check_downgrade_prerequisite

	if [[ $running_freebsd_ns == 1 ]]; then
		# NOTE: applicable for F2L only. for F2F, refer template.pl
		_check_freebsd_sysid
		_check_freebsd_vpx
	elif [[ $running_linux_ns == 1 ]]; then
		_check_linux_sysid
		_check_linux_vpx
		[[ $NSVARIANT == "NSLINUX" ]] && _check_nsconf
	elif [[ $running_linux_ns_installer == 1 ]]; then
		debug "resuming in ns installer, no pre-checks needed."
	else
		error "running on an unknown OS, not supported"
	fi

	_check_var_free_space

	if [ ${#precheck_fmsgs[@]} -ne 0 ]; then
		for msg in "${precheck_fmsgs[@]}"; do
			info "PRE-CHECK FAILED: $msg"
		done
		error "NetScaler pre-checks failed"
	fi
}

_check_installer() {
	 [[ $NSVARIANT == "v" ]] && return
	info_step "Check installer files"
	run 'test -f "$installer_files_path"' || error "'$installer_files_path' not found"
	while read line; do
		file=$(echo $line | awk '{for (i=2; i<=NF; i++) print $i}')
		file="${install_dirpath}/${file}"
		sha256=$(echo $line | awk '{print $1}')
		if [ ! -f "$file" ]; then
			error "Installer file '$file' not found"
		fi

		if [[ $running_freebsd_ns == 1 ]]; then
			cur_sha256=$(sha256 -q "$file")
		elif [[ $running_linux_ns == 1 ]] || [[ $running_linux_ns_installer == 1 ]]; then
			cur_sha256=$(sha256sum "$file" | awk '{print $1}')
		fi
		if [[ $cur_sha256 != $sha256 ]]; then
			debug "$file -> $sha256 != $cur_sha256"
			error "Installer file '$file' checksum mismatch"
		fi
	done < "$installer_files_path"
	info "Done"
}

_check_installer_compatibility() {
	info_step "Check installer compatibility"
	if [[ $NSVARIANT == "v" ]] && [[ $running_linux_ns == 1 ]]; then
		debug "L2F upgrade compatibility check"
		if [[ ! -f "/flash/boot/loader.conf" ]]; then
			info "Failed"
			error "NetScaler upgraded from freeBSD variants is required for downgrade back to freeBSD"
		fi
		# Feature exists from NS14.1 Build 56.*.nc onwards, no check required.
	elif [[ $NSVARIANT == "NSLINUX" ]] && [[ $running_freebsd_ns == 1 ]]; then
		debug "F2L upgrade compatibility check"
		running_nsversion="$(sysctl -n netscaler.version)"
		running_nsversion_rel=$(echo $running_nsversion | awk -F 'NS' '{print $2}' | awk -F ':' '{print $1}')
		running_nsversion_ver=$(echo $running_nsversion | awk -F "Build " '{print $2}' | awk -F ".nc" '{print $1}')
		running_nsversion_rel_A=$(echo $running_nsversion_rel | awk -F "." '{print $1}')
		running_nsversion_rel_B=$(echo $running_nsversion_rel | awk -F "." '{print $2}')
		running_nsversion_ver_A=$(echo $running_nsversion_ver | awk -F "." '{print $1}')

		if [[ $running_nsversion_rel_A -lt $TRANSIT_BUILD_REL_A ]] ||
				([[ $running_nsversion_rel_A -eq $TRANSIT_BUILD_REL_A ]] &&
					[[ $running_nsversion_rel_B -lt $TRANSIT_BUILD_REL_B ]]) ||
				([[ $running_nsversion_rel_A -eq $TRANSIT_BUILD_REL_A ]] &&
					[[ $running_nsversion_rel_B -eq $TRANSIT_BUILD_REL_B ]] &&
					[[ $running_nsversion_ver_A -lt $TRANSIT_BUILD_VER_A ]]); then
			info "Failed"
			error "NetScaler Installer of variant 'lx' is not compatible with running NetScaler version" \
				"$running_nsversion_rel Build $running_nsversion_ver.nc. Please upgrade to NetScaler version" \
				"14.1 Build 56.*.nc or later before switching to lx variant (base operating system -> Linux)"
		fi
	fi
	info "Done"
}

## UPGRADE FUNCTIONS
_populate_system_info() {
	MAX_ALLOWED_BACKUP_SIZE_K=$(( 5 * 1024 * 1024 )) # only applicable for l2f and f2l
	MIN_FREE_K=$((                      16 * 1024 ))
	PART_SHRINK_SIZE_BUFFER_K=$((       16 * 1024 ))
	L2F_BACKUP_PART_MAXSIZE_K=$(( 3 * 1024 * 1024 ))

	INIT_FLAG_PREFIX="/flash/.init."
	INIT_NSINSTALL_FLAG="$INIT_FLAG_PREFIX"nsinstall
	# clone resume_file to INIT_RESUME_FLAG for ns-installer to resume installns on boot
	INIT_RESUME_FLAG="$INIT_FLAG_PREFIX"resume
	INIT_VAR_RESUME_FLAG="/var/.init.resume"

	flash_dir=/flash
	var_dir=/var
	tmp_suffix=".ns.tmp"
	backup_suffix=".ns.bak"
	tmpdir=/var/tmp
	grub_cfg=/flash/boot/grub/grub.cfg
	supportbundle_dir=/var/tmp/support
	supportbundle=$supportbundle_dir/support.tgz

	out_mount=$(mount)
	dev_root=$(echo "$out_mount" | grep "on / " | awk '{print $1}')
	dev_flash=$(echo "$out_mount" | grep "on /flash " | awk '{print $1}')
	dev_var=$(echo "$out_mount" | grep "on /var " | awk '{print $1}')

	if [[ "$install_mode" == "f2l" ]]; then
		var_path_to_backup_restore=()
		if [[ "$install_dirpath_original" == "/var/"* ]]; then
			while read line; do
				file=$(echo "$line" | awk '{for (i=2; i<=NF; i++) print $i}')
				var_path_to_backup_restore+=("$install_dirpath_original/$file")
			done < "$install_dirpath/ns-installer.files"
			var_path_to_backup_restore+=("$install_dirpath_original/ns-installer.files")
		fi
		var_path_to_backup_restore+=("$supportbundle")
		for file in "${DATA2RETAIN[@]}"; do
			if [[ "$file" == "/var/"* ]]; then
				var_path_to_backup_restore+=("$file")
			fi
		done

		flash_path_to_backup_restore=()
		if [[ "$install_dirpath_original" == "/flash/"* ]]; then
			while read line; do
				file=$(echo "$line" | awk '{for (i=2; i<=NF; i++) print $i}')
				flash_path_to_backup_restore+=("$install_dirpath/$file")
			done < "$install_dirpath/ns-installer.files"
			flash_path_to_backup_restore+=("$install_dirpath/ns-installer.files")
		fi
		for file in "${DATA2RETAIN[@]}"; do
			if [[ "$file" == "/flash/"* ]]; then
				flash_path_to_backup_restore+=("$file")
			fi
		done
	elif [[ "$install_mode" == "l2f" ]]; then
		l2f_backup_dir="l2f_backup"
		mp_l2f_backup1="/l2f_backup1"
		mp_l2f_backup2="/l2f_backup2"
		fdisk_data_file="/flash/l2f_fdisk_disk0_table"

		# l2f: install_dirpath is not mentioned but is explicitly backed up and restored.
		var_path_to_backup_restore=()
		var_path_to_backup_restore+=("$supportbundle")
		for file in "${DATA2RETAIN[@]}"; do
			if [[ "$file" == "/var/"* ]]; then
				var_path_to_backup_restore+=("$file")
			fi
		done

		flash_path_to_backup_restore=()
		for file in "${DATA2RETAIN[@]}"; do
			if [[ "$file" == "/flash/"* ]]; then
				flash_path_to_backup_restore+=("$file")
			fi
		done

	fi
}

_populate_system_info_freebsd() {
	out_df_k=$(df -k)
	out_gpart_p=$(gpart show -p)
	out_swapinfo=$(swapinfo)

	disk0_info=$(echo "$out_gpart_p" | head -n 1)
	disk0=$(echo "$disk0_info" | awk '{print $4}')

	geoms=$(echo "$out_gpart_p" | grep "freebsd " | awk '{print $3}')
	geom0=""
	for geom in $geoms; do
		if [[ "$dev_flash" == "/dev/$geom"* ]]; then
			geom0=$geom
			break
		fi
	done
}

_populate_system_info_linux() {
	disk0=$(lsblk -no pkname $dev_root)
	out_fdisk_l=$(fdisk -l /dev/"$disk0")

	boot_dir=/boot
	storage_dir=/storage
	var_partition_label=storage
	swap_next_flash=""

	# environ file is in bad shape where it can't be fully sourced.
	eval "$(grep "NS_LINUX_SYSID" /root/.blx/environ)"
}

_populate_system_info_linux_installer() {
	#ns-installer startup script will pass the required environment variables
	disk0=${disk0:-xvda}

	out_fdisk_l=$(fdisk -l /dev/$disk0)

	boot_dir=/boot
	flash_dir=/flash
	storage_dir=/storage
	freebsd_backup_dir=/var/freebsd-backup
	var_partition_label=storage

	partnbr_extended=4
	dev_extended=/dev/"$disk0"$partnbr_extended
	partition_info_f="${install_dirpath}/partition_info.txt"

	# Extract the first partition line and deduce suffix
	first_part=$(echo "$out_fdisk_l" | grep -m 1 "^/dev/${disk0}[0-9]" | awk '{print $1}')
	if echo "$first_part" | grep -q "^/dev/${disk0}p[0-9]"; then
		part_suffix="p"  # e.g., /dev/nvme0n1p1
	else
		part_suffix=""   # e.g., /dev/xvda1
	fi

	if [[ -f "$resume_file" ]]; then
		debug "running in linux-installer, keep resume flag till done"
		run 'cp "$resume_file" "$INIT_RESUME_FLAG"'
	fi
}

_print_banner() {
#<------------MAX ALLOWED WIDTH 79 CHARACTERS-------------------------------->#
	if [[ "$install_mode" == "f2l" ]]; then
		cat <<EOF

NOTE: NetScaler's operating system will be switched from FreeBSD to Linux.
EOF
	elif [[ "$install_mode" == "l2f" ]]; then
		cat <<EOF

NOTE: NetScaler's operating system will be switched from Linux to FreeBSD.
EOF
	fi

	if [[ "$install_mode" == "f2l" ]] || [[ "$install_mode" == "l2f" ]]; then
		cat <<EOF
CAUTION: disk will be re-partitioned, non-NetScaler data will be lost and all secondary disks will be formatted and re-partitioned.
- NetScaler will retain NetScaler configuration (/flash/nsconfig), NetScaler
  logs (/var/log, /var/nslog) and running installer files.
- NetScaler upgrade will requires multiple reboots during the upgrade.
EOF
	fi

	if [[ "$install_mode" == "f2l" ]]; then
		cat <<EOF
- A technical support bundle will be collected now and retained post upgrade.
EOF
	fi

	cat <<EOF

NetScaler version $NSVERSION checksum and signing public key are located
on https://www.citrix.com/downloads under NetScaler > Firmware.
Select the Release $NSVERSION link and expand the "Build" link to view SHA2
checksum and digital signing public key for build $NSVERSION.
EOF

	if [[ $OPTION_ASSUME_YES == true ]]; then
		echo "[Option 'Y' set, Continuing with upgrade ....]"
	else
		_confirmation_prompt "Are you sure you want to continue with the upgrade?" "n" 211
	fi
}

_print_warning() {
	if [[ $OPTION_ASSUME_YES == true ]]; then
		return
	fi

	if [[ "$install_mode" == "f2l" ]] || [[ "$install_mode" == "l2f" ]]; then
		cat <<EOF

CAUTION: ALL NON-NETSCALER DATA WILL BE LOST.
If you want to take a backup of the NetScaler configuration or any additional
data, exit installer now. You can start installer later again.
Note: NetScaler configuration will be preserved, but it is always recommended
to take external backup of the configuration before NetScaler upgrade.
EOF
		_confirmation_prompt "Do you want to continue with the upgrade?" "n" 212
	fi
}

_collect_techsupport_bundle() {
	if [ -d "$supportbundle_dir" ]; then
		debug "delete old techsupport bundle(s)"
		rm -rf "$supportbundle_dir" || warn "failed to delete old techsupport bundle reference"
	fi
	info_step "Collect techsupport bundle"
	run 'PATH=/netscaler:$PATH /netscaler/showtechsupport.pl'
	if [ ! -e $supportbundle ]; then
		info "Failed"
		warn "failed to collect techsupport bundle"
		return
	fi
	debug "techsupport bundle info: $(ls -Ll $supportbundle)"
	info "Done"
}

_f2l_init_info_p1() {
	partinfo_after_flash=$(echo "$out_gpart_p" | grep -A1 $(basename $dev_flash) | tail -n1)
	if [[ $(echo "$partinfo_after_flash" | grep -c "freebsd-swap") == 1 ]]; then
		debug "partinfo_after_flash=$partinfo_after_flash"
		swap_next_flash=/dev/$(echo "$partinfo_after_flash" | awk '{print $3}')
	fi

	_set_partnbr partnbr_flash $geom0 $dev_flash 1
	_set_partnbr partnbr_var $geom0 $dev_var 5
}

_f2l_flash_clean_obsolete_files() {
	freebsd_loader_conf=/flash/boot/loader.conf
	signature_fname_suffix=".sha512.signed.bin"
	kernel_dirpath="/flash"

	eval "freebsd_active_$(grep '^kernel=' $freebsd_loader_conf | tail -n1).gz"
	if [ -z "$freebsd_active_kernel" ]; then
		warn "failed to detect active freebsd kernel, skip cleanup on flash"
		return 1
	fi
	freebsd_active_kernel=$(realpath "$kernel_dirpath/$freebsd_active_kernel")
	if [ -z "$freebsd_active_kernel" ] || [ ! -f "$freebsd_active_kernel" ]; then
		warn "active freebsd kernel missing on flash???, skip cleanup on flash"
		return 2
	fi

	info_step "Delete all old signature files and associated kernel images"
	valid_signature_file_patterns=("/flash/secboot/*$signature_fname_suffix"
		"/flash/nsconfig/*$signature_fname_suffie")
	for sig_file in ${validsignature_file_patterns[@]}; do
		if [ -z "$sig_file" ] || [ ! -f "$sig_file" ]; then
			continue
		fi

		sig_fname="$(basename $sig_file)"
		sig_kernel_fname="${sig_fname%$valid_signature_fname_suffix}"
		sig_kernel_file="$(realpath $kernel_dirpath/$sig_kernel_fname)"
		debug " Signature File: '$sig_file', Kernel File: '$sig_kernel_file'"
		if [ -z "$sig_kernel_file" ] || [ ! -f "$sig_kernel_file" ]; then
			debug "kernel not found for signature '$sig_file': '$sig_kernel_file'"
			continue
		fi

		if [[ "$freebsd_active_kernel" == "$sig_kernel_file" ]]; then
			debug "active kernel: '$sig_kernel_file', skip deletion"
			continue
		fi

		debug "deleting '$sig_file' -> '$sig_kernel_file'"
		rm -f "$sig_file"
		rm -f "$sig_kernel_file"
	done

	# Clean backup dirs
	rm -rf "/flash/var" "/var/flash"

	sync
	info "Done"
}

_f2l_check_flash_space() {
	info_step "Check required free space"
	out_df_k_flash=$(df -k /flash | grep "/flash")
	flash_free_k=$(echo "$out_df_k_flash" | awk '{print $4}')

	# calculate space expected post flash extend
	swap_free_k=0
	if [ -n "$swap_next_flash" ]; then
		swap_free_bytes=$(gpart list $geom0 | grep -A1 "Name: $(basename $swap_next_flash)" | grep "Mediasize: " | awk '{print $2}')
		swap_free_k=$(( swap_free_bytes / 1024 )) || error "Failed to calculate swap space"
	fi

	free_k=0
	if [[ $(echo "$partinfo_after_flash" | grep -c " \- free \- ") == 1 ]]; then
		free_sectors=$(echo "$partinfo_after_flash" | awk '{print $2}')
		sector_size=$(gpart list $geom0 | grep -A2 "Name: $geom0$" | grep "Sectorsize" | awk '{print $2}')
		if [ $free_sectors ] && [ $sector_size ]; then
			free_k=$(( free_sectors * $sector_size / 1024 ))
		fi
	fi

	out_du_Lkc_var_path_to_backup_restore=$(du -sLkc "${var_path_to_backup_restore[@]}")
	debug "du output: '$out_du_Lkc_var_path_to_backup_restore'"
	backup_size_k=$(echo "$out_du_Lkc_var_path_to_backup_restore" | tail -n1 | awk '{print $1}')
	debug "backup size: $backup_size_k KB"

	flash_space_needed=$(( backup_size_k + MIN_FREE_K )) || error "Failed to compute space needed"
	space_available=$(( flash_free_k + swap_free_k + free_k )) || error "Failed to compute space available"
	debug "required free space on /flash : $flash_space_needed KB"
	debug "available free space on /flash: $flash_free_k KB"
	debug "available free space from swap: $swap_free_k KB"


	if [ $flash_space_needed -gt $space_available ]; then
		info "Failed"
		space_to_clean_mb=$(( 1 + (flash_space_needed - space_available) / 1024 ))
		warn "backup requires additional $space_to_clean_mb MB."
		info "---------------------------------------------------------------"
		info "Components to review manually (du -sLh <path>):"
		du -sLh "${DATA2RETAIN[@]}" | sort -h
		info "---------------------------------------------------------------"
		error "Please manually delete atleast $space_to_clean_mb MB in total from listed paths"
	fi
	info "Done"
}

_f2l_swapoff_next_flash() {
	if [[ $(echo "$out_swapinfo" | grep -c $swap_next_flash) == 1 ]]; then
		info_step "Disable swap on $swap_next_flash"
		swapoff $swap_next_flash && info "Done" && return
		info "Swap in use"

		sysctl_ns__num_pe_running=$(sysctl -n netscaler.num_pe_running)
		if [[ $sysctl_ns__num_pe_running != 0 ]]; then
			_reboot
		fi
	fi
}

_f2l_flash_extend() {
	if [[ -e "$swap_next_flash" ]] && [[ $(swapinfo | grep -c $swap_next_flash) == 0 ]]; then
		debug "swap was detected and turned off on $swap_next_flash"
	fi

	if [[ $(echo "$partinfo_after_flash" | grep -c " \- free \- ") == 1 ]]; then
		debug "free space exist after flash partition"
	fi

	partnbr_start=$(( partnbr_flash + 1 ))
	partnbr_end=$(( partnbr_var - 1 ))
	if [[ -z "$partnbr_start" ]] || [[ -z "$partnbr_end" ]] || [[ ! $partnbr_start < $partnbr_end ]]; then
		debug "Unexpected freebsd slice/partition state, skipping flash extend..."
		return 1
	fi

	info_step "Extend flash partition"
	for (( partnbr=partnbr_start; partnbr<=partnbr_end; partnbr++ )); do
		if [[ $partnbr == 3 ]]; then
			# partition 3 represents whole slice
			continue
		fi
		part_alphabet=$(echo "$partnbr" | tr 1-9 "a-z")
		if [[ ! -e "/dev/$geom0$part_alphabet" ]]; then
			debug "Partition /dev/$geom0$part_alphabet does not exist, skipping"
			continue
		fi
		debug "$geom0 -> Partition '$partnbr' to be deleted"
		run gpart delete -i $partnbr $geom0 || (info "Reboot required" && _reboot)
	done

	run gpart resize -i $partnbr_flash $geom0 || warn "ignored resize partition $partnbr_flash"
	run growfs -y $dev_flash || warn "ignored grow filesystem for $dev_flash"
	info "Done"
}

_f2l_var2flash_backup() {
	info_step "Backup data"
	for file in "${var_path_to_backup_restore[@]}"; do
		parent_dir=$(dirname "$file")
		if [ -d "$file" ]; then
			rm -rf "/flash/$file.tgz"
			mkdir -p "/flash/$parent_dir"
			filename=$(basename "$file")
			run 'cd "$parent_dir" && tar cvfz "/flash/$file.tgz" "$filename"' \
				|| warn "failed to create backup /flash/$file.tgz"
		elif [ -e "$file" ]; then
			rm -rf "/flash/$file"
			mkdir -p "/flash/$parent_dir"
			run 'cp -vf "$file" "/flash/$file"' || warn "failed to backup file $file"
		else
			debug "missing $file"
		fi
	done
	info "Done"
}

_f2l_grub_install() {
	grub_cfg_dir=$(dirname "$grub_cfg")
	mkdir -p "$grub_cfg_dir"
	cat > "$grub_cfg" <<EOF
set default="1"
set timeout="0"
set root=(hd0,msdos1)

menuentry "NetScaler (FreeBSD) (Pre-upgrade build)" {
        chainloader +1
}

menuentry "Resume NetScaler upgrade (build version $NSVERSION)" {
        linux "(hd0,msdos1)$install_dirpath/$bzimage_name" console=ttyS0,115200 console=tty1 rootwait init=/init NSUPGRADE selinux=1 enforcing=0 rw
        initrd "(hd0,msdos1)$install_dirpath/$initrd_name"
}
EOF
	test -f "$install_dirpath/grub.tgz" || error "grub binaries archive is missing"

	info_step "Install grub"
	rm -rf "$tmpdir/grub" && mkdir -p "$tmpdir"
	tar xpfz "$install_dirpath/grub.tgz" -C "$tmpdir" || \
		error "Failed to extract grub binaries"
	"$tmpdir/grub/sbin/grub-install" -d "$tmpdir/grub/lib/grub/i386-pc" \
		--boot-directory=/flash/boot "/dev/$disk0" || \
		error "Failed to install grub bootloader"
	info "Done"
}

_create_linux_partition_from_partition_info() {
	local partition_lines="$1" target_dev="/dev/$disk0"

	# Process each line from the input variable
	echo "$partition_lines" | awk -F, '
	{
		label = $1
		part_num = $2
		fs_type = $3
		start_sector = $4
		end_sector = $5
		if (part_num < 4) partition_type = "primary"
		else if (part_num == 4) partition_type = "extended"
		else partition_type = "logical"
		print "mkpart " partition_type " " start_sector "s " end_sector "s " part_num " " fs_type " " label
	}
	' | while read -r mkpart ptype start end part_num fs_type label; do
		# Create partition
		debug "Creating Partition:: parted -s $target_dev $mkpart $ptype $start $end"
		run 'parted -s "$target_dev" "$mkpart" "$ptype" "$start" "$end"' \
			|| warn " Failed to create partition $part_num on $target_dev"

		part_dev="${target_dev}${part_suffix}${part_num}"

		# Apply filesystem and label based on FS Type
		case "$fs_type" in
		83)
			debug "Formatting $part_dev as ext4"
			run 'mkfs.ext4 -F "$part_dev"' || warn "Failed to format $part_dev as ext4"
			debug "Applying label '$label' to $part_dev"
			run 'e2label "$part_dev" "$label"' || warn "Failed to apply label '$label' to $part_dev"
			;;
		ef)
			debug "Formatting $part_dev as FAT32"
			run 'mkfs.fat -F 32 "$part_dev"' || warn "Failed to format $part_dev as FAT32"
			debug "Applying label '$label' to $part_dev"
			run 'fatlabel "$part_dev" "$label"' || warn "Failed to apply label '$label' to $part_dev"
			;;
		82)
			debug "Formatting $part_dev as swap"
			run 'parted -s "$target_dev" set $part_num swap on'
			run 'mkswap "$part_dev"' || warn "Failed to format $part_dev as swap"
			;;
		f)
			debug "Partition $part_dev is extended, no filesystem applied"
			;;
		esac
	done
}

_get_bsd_partition_data() {
	# Get BSD partition table using fdisk
	local dev_disk="$1"
	(
		echo b  # bsd
		echo p  # print
		echo q  # quit
	) | fdisk "$dev_disk"
}

_f2l_remove_freebsd_var_partition() {
	#Pre-exec: Check BSD partition table.
	#exec:Remove all partitions aprt from flash(a) and freebsd slice(c)
	#Post-exec: Verify only BSD partition remains
	info_step "Remove FreeBSD /var partition"

	#Get initial BSD partition data
	local out_fdisk_bsd
	out_fdisk_bsd=$(_get_bsd_partition_data "/dev/$disk0")

	#Extract partition list (reversed order with tac)
	local bsd_part_list
	bsd_part_list=$(echo "$out_fdisk_bsd" | awk '/[a-z]\s+[0-9]+\s+[0-9]+\s+[0-9]+\s+[0-9.]+G/ {print $1, $5, $6; print ""}' | tac)

	# Process each partition and remove bsd partitions matching above mentioned criteria
	echo "$bsd_part_list" | while read -r bsd_part size type; do
		[ -z "$bsd_part" ] && continue

		size=${size%G}
		debug "Checking partition $bsd_part: Size = $size, Type = $type"

		if [ "$bsd_part" = "a" ] || [ "$bsd_part" = "c" ]; then #Delete all partitions blindly apart from flash(a) and bsd slice partition(c)
			continue
		else
			debug "Deleting partition $bsd_part (Reason: $type ${size}GB)"
			(
			echo b            #bsd
			echo d $bsd_part  #delete part
			echo $bsd_part    #confirm part
			echo p            #print
			echo w            #write
			) | fdisk "/dev/$disk0" >> "$logfile" 2>&1
		fi
	done
	info "Done"
}

_f2l_shrink_freebsd_slice() {
	#Pre-exec: Check only one BSD partition present
	#exec:Get last sector of BSD partition . And resize entire slice end sector to last BSD part sector+100s(buffer similar to freebsd image)
	info_step "Shrink freebsd slice"

	# Get BSD partition data
	local out_fdisk_bsd
	out_fdisk_bsd=$(_get_bsd_partition_data "/dev/${disk0}")

	#Count BSD partitions
	local bsd_part_count
	bsd_part_count=$(echo "$out_fdisk_bsd" | awk '/4.2BSD/ {count++} END {print count+0}')

	# Check if exactly one BSD partition exists
	[[ "$bsd_part_count" -ne 1 ]] && return

	# Get last sector and resize
	local last_sector
	last_sector=$(echo "$out_fdisk_bsd" | awk '/4.2BSD/ {print $3}')
	run '"yes" | parted "/dev/${disk0}" ---pretend-input-tty resizepart 1 $((last_sector + 100))s' || warn "Resize warnings ignored"
	info "Done"
}

_create_linux_post_extended_partition() {
	#exec:Creates dummy partitions which will be later removed to create actual linux partitions.
	#dummy partitions are created to match disk layout same as VPX-Linux fresh provision
	#Creates extended partition and logical partitions
	info_step "Create extended and logical partitions"

	# Get fdisk output and extract last sector
	out_fdisk=$(fdisk -l "/dev/$disk0")
	last_sector=$(echo "$out_fdisk" | awk '/FreeBSD/ {print $4}')

	[[ -z "$last_sector" ]] && error "No FreeBSD partition found or unable to determine end sector."
	if ! blkid | grep -q 'LABEL="$var_partition_label"' | grep "$disk0"; then
		# Create dummy partitions with parted (non-interactive)
		run 'parted -s "/dev/$disk0" mkpart primary ext4 "$((last_sector + 100))s" "$((last_sector + 199))s"'
		run 'parted -s "/dev/$disk0" mkpart primary ext4 "$((last_sector + 200))s" "$((last_sector + 299))s"'

		#Create all partitions from extended partition table
		# Extract lines from 'extended' onward into partition_info
		partition_info=$(tail -n +2 "$partition_info_f" | awk -F, '
			$1 == "extended" { start_processing = 1 }
			start_processing { print $0 }')

		#Create linux partitions from partition info
		_create_linux_partition_from_partition_info "$partition_info"
	fi
	info "Done"
}

_mount_var_partition(){
	mkdir -p /storage && mount -L storage /storage || warn "Failed to mount storage partition"
	mkdir -p /storage/var
	mount --bind /storage/var /var || warn "Failed to bind mount /storage/var to /var"
}

_unmount_var_partition(){
	run 'umount -f /var'
	run 'umount -f /storage'
}

_f2l_var_restore() {
	info_step "Restore backup into linux /var partition"
	_mount_var_partition

	for file in "${var_path_to_backup_restore[@]}"; do
		# file contains destination absolute path
		if [[ "$file" != "/var"* ]]; then
			warn "Not sure how to restore $file"
			continue
		fi
		backup_file="/flash/$file"
		parent_dir=$(dirname "$file")
		mkdir -p "$parent_dir"
		if [ -f "$backup_file" ]; then
			run 'cp -vf "$backup_file" "$file"' || warn "failed to restore file $file"
		elif [ -f "$backup_file".tgz ]; then
			filename=$(basename "$file")
			run 'cd "$parent_dir" && tar xvfz "$backup_file.tgz"' \
				|| warn "failed to restore dir $file"
		else
			debug "missing $file"
		fi
	done
	sync
	info "Done"
}

_f2l_flash_backup() {
	info_step "Backup FreeBSD /flash partition data"
	for file in "${flash_path_to_backup_restore[@]}"; do
		parent_dir=$(dirname "$file")
		if [ -d "$file" ]; then
			rm -rf "/var/$file.tgz"
			mkdir -p "/var/$parent_dir"
			filename=$(basename "$file")
			run 'cd "$parent_dir" && tar cvfz "/var/$file.tgz" "$filename"' \
				|| warn "failed to create backup /var/$file.tgz"
		elif [ -e "$file" ]; then
			rm -rf "/var/$file"
			mkdir -p "/var/$parent_dir"
			run 'cp -vf "$file" "/var/$file"' || warn "failed to backup file $file"
		else
			debug "missing $file"
		fi
	done
	info "Done"
}

_install_grub_to_var() {
	info_step "Install grub on var partition"
	# Before deleting flash partitions, install grub on var partition
	# else we can end up in a non-bootable state
	grub_cfg="/storage/boot/grub/grub.cfg"
	grub_cfg_dir=$(dirname "$grub_cfg")
	mkdir -p "$grub_cfg_dir"
	cat > "$grub_cfg" <<EOF
set default="0"
set timeout="0"
set root=(hd0,msdos6)

menuentry "Resume NetScaler upgrade (build version $NSVERSION)" {
        linux "(hd0,msdos6)$install_dirpath/$bzimage_name" console=ttyS0,115200 console=tty1 rootwait init=/init selinux=1 enforcing=0 rw
        initrd "(hd0,msdos6)$install_dirpath/$initrd_name"
}
EOF
	run 'grub-install --boot-directory=/storage/boot "/dev/$disk0"' \
		|| warn "Failed to install grub bootloader on storage"
	info "Done"
}

_create_linux_pre_extended_partition() {
	info_step "Remove obsolete partitions"
	umount -lf "$flash_dir"
	local reboot_required=0
	for ((i = 1; i < 4; i++)); do #extended partition will be the 4th entry always
		if [[ -n "$(lsblk -no NAME /dev/${disk0}${part_suffix}${i})" ]]; then
			run 'parted -s /dev/"$disk0" rm $i' || reboot_required=1
		fi
	done
	info "Done"

	if [[ $reboot_required == 1 ]]; then
		info "Reboot is required for further installation"
		_reboot
	fi

	info_step "Create linux partitions"
	#Create linux partitions before extended partition
	partition_info=$(tail -n +2 "$partition_info_f" | awk -F, '
		$1 == "extended" { exit }
		{ print $0 }')
	_create_linux_partition_from_partition_info "$partition_info"
	info "Done"
}

_install_linux_artifacts() {
	info_step "Install new artifacts"
	boot_dev=/dev/"${disk0}1"
	flash_dev=/dev/"${disk0}2"

	mkdir -p "$boot_dir" "$flash_dir" || error "Failed to create mount points"
	mount | grep -q "${boot_dir}" || mount "$boot_dev" "$boot_dir" || error "Mount $boot_dir failed"
	mount | grep -q "${flash_dir}" || mount "$flash_dev" "$flash_dir" || error "Mount $flash_dir failed"

	debug "Copy artifacts"
	run 'cp -f "$install_dirpath/$bzimage_name" "$boot_dir/"' || error "Copy bzImage failed"
	run 'cp -f "$install_dirpath/$initrd_name" "$flash_dir/"' || error "Copy initrd failed"
	run 'cp -f "$install_dirpath/$rootfs_name" "$flash_dir/"' || error "Copy rootfs failed"

	debug "Copy public key and signatures"
	mkdir -p "$boot_dir/secboot" "$flash_dir/secboot"
	run 'cp -f "$install_dirpath/$bzimage_name".sha512.signed "$boot_dir/secboot/"'
	run 'cp -f "$install_dirpath/$initrd_name".sha512.signed  "$flash_dir/secboot/"'
	run 'cp -f "$install_dirpath/$rootfs_name".sha512.signed  "$flash_dir/secboot/"'
	run 'cp -f "$install_dirpath/$rsapubkey_name"             "$flash_dir/secboot/"'

	debug "move /etc/blx/<persistent_files> to /flash/nsconfig/"
	etc_blx_persist_fnames=("aslearn.linux.conf" "blx_install_date" "blx-lic-mac-file")
	for blx_fname in "${etc_blx_persist_fnames[@]}"; do
		if [ -f "/etc/blx/$blx_fname" ]; then
			mv -f "/etc/blx/$blx_fname" "$flash_dir/nsconfig/$blx_fname"
		fi
	done

	debug "Grub config"
	mkdir -p "$boot_dir/boot/grub" \
		&& run 'cp -f "$install_dirpath/grub.cfg" "$boot_dir/boot/grub/grub.cfg"' \
		|| error "Copy grub config failed"
	mkdir -p "$boot_dir/EFI/BOOT/" \
		&& run 'cp -f "$install_dirpath/grub.cfg" "$boot_dir/EFI/BOOT/grub.cfg"' \
		|| error "Failed to copy grub.cfg to EFI partition"

	debug "set initrd mode to nsinstall"
	rm -f "$INIT_FLAG_PREFIX"*
	touch "$INIT_NSINSTALL_FLAG"
	info "Done"
}

_f2l_flash_restore() {
	_mount_var_partition
	info_step "Restore data"
	for file in "${flash_path_to_backup_restore[@]}"; do
		# file contains destination absolute path
		if [[ "$file" != "/flash"* ]]; then
			warn "Not sure how to restore $file"
			continue
		fi
		backup_file="/var/$file"
		parent_dir=$(dirname "$file")
		mkdir -p "$parent_dir"
		if [ -f "$backup_file" ]; then
			run 'cp -vf "$backup_file" "$file"' || warn "failed to restore file $file"
		elif [ -f "$backup_file".tgz ]; then
			filename=$(basename "$file")
			run 'cd "$parent_dir" && tar xvfz "$backup_file.tgz"' \
				|| warn "failed to restore dir $file"
		else
			debug "missing $file"
		fi
	done
	info "Done"
}

_install_grub_to_boot() {
	info_step "Install grub"
	/sbin/grub-install --boot-directory="$boot_dir/boot" --target=i386-pc /dev/"$disk0" \
		&& info "Done" || error "GRUB installation failed on $disk0"

	debug "Setting boot flag on ${disk0}"
	parted -s /dev/"$disk0" set 1 boot on || warn "Failed to set boot flag on ${disk0}"

	umount "$boot_dir" || warn "Failed to unmount boot directory"

	# clean obsolete grub boot files
	rm -rf /storage/boot
}

_delete_older_linux_images() {
	info_step "Clean obsolete files"

	# retain max 2 netscaler versions, current active one for revert and install artifacts coming via upgrade.
	debug "delete non-active kernel, initrd, rootfs"
	local active_kernel_version=$(grep -o 'bzImage-[^ ]\+' /proc/cmdline | sed "s;bzImage-;;g")
	local files2retain=("/boot/bzImage-$active_kernel_version"
		"/flash/initrd-$active_kernel_version.cpio.gz"
		"/flash/rootfs-$active_kernel_version.tar.gz")
	local files2clean=$(ls /boot/bzImage* /flash/initrd*.cpio.gz /flash/rootfs*.tar.gz 2> /dev/null)
	for file in $files2clean; do
		[[ " ${files2retain[@]} " =~ " $file " ]] && continue
		debug "removing stale file '$file'"
		rm -f "$file"
	done

	debug "delete stale signatures"
	local signs=$(ls /boot/secboot/*.sha512.signed /flash/secboot/*.sha512.signed 2> /dev/null)
	for sign in $signs; do
		local sign_basename=$(basename "$sign")
		local sign_dir=$(dirname "$sign")
		local fpath="$sign_dir/../${sign_basename%.sha512.signed}"
		if [[ ! -f "$fpath" ]]; then
			debug "delete '$sign'"
			rm -f "$sign"
		fi
	done

	debug "clean backup dirs"
	rm -rf "/flash/var" "/var/flash"
	sync
	info "Done"
}

_l2f_init_info_p1() {
	partinfo_after_flash=$(echo "$out_fdisk_l" | grep -A1 "$(basename "$dev_flash")" | tail -n1)

	# Check if the partition type is "Linux swap" (this should match "82 Linux swap" or "Linux swap / Solaris").
	if [[ $(echo "$partinfo_after_flash" | grep -c "Linux swap") -eq 1 ]]; then
		debug "partinfo_after_flash=$partinfo_after_flash"
		# For the fdisk output, the device name is in the first column.
		swap_next_flash=$(echo "$partinfo_after_flash" | awk '{print $1}')
	fi

	partnbr_flash=${dev_flash: -1}
	out_parted_free_disk0=$(parted -m /dev/"$disk0" "unit kiB print free")
	out_parted_part_after_flash=$(echo "$out_parted_free_disk0" | grep -A1 ":ext4:" | grep -A1 "^$partnbr_flash:" | tail -n1)
}

_l2f_check_flash_space() {
	info_step "Check required free space"
	out_df_k_flash=$(df -k /flash | grep "/flash")
	flash_free_k=$(echo "$out_df_k_flash" | awk '{print $4}')

	swap_free_k=0
	if [ -e "$swap_next_flash" ]; then
		swap_free_k=$(echo "$out_parted_part_after_flash" | awk -F: '{print $4}')
		swap_free_k=${swap_free_k%"kiB"}
		[[ $(( swap_free_k+1-1 )) == $swap_free_k ]] || (swap_free_k=0 && warn "failed to fetch swap space")
	fi

	free_k=0
	if [[ $(echo "$out_parted_part_after_flash" | grep -c ":free;") == 1 ]]; then
		free_k=$(echo "$out_parted_part_after_flash" | awk -F: '{print $4}')
		free_k=${free_k%"kiB"}
		[[ $(( free_k+1-1 )) == $free_k ]] || (free_k=0 && warn "failed to fetch free space")
	fi

	# we need to move only installer files to flash
	out_du_kc_installer_dir=$(du -kc "$install_dirpath")
	backup_size_k=$(echo "$out_du_kc_installer_dir" | tail -n1 | awk '{print $1}')

	flash_space_needed=$(( backup_size_k + MIN_FREE_K )) || error "Failed to compute space needed"
	space_available=$(( flash_free_k + swap_free_k + free_k )) || error "Failed to compute space available"
	debug "required free space on /flash : $flash_space_needed KB"
	debug "available free space on /flash, swap or free: $flash_free_k KB, $swap_free_k KB, $free_k KB"

	if [ $flash_space_needed -gt $space_available ]; then
		info "Failed"
		space_to_clean_mb=$(( 1 + (flash_space_needed - space_available) / 1024 ))
		warn "Upgrade requires additional $space_to_clean_mb MB on /flash."
		error "Please manually delete atleast $space_to_clean_mb MB in total from /flash"
	fi

	# check overall backup size for limit
	out_du_Lkc_backup=$(du -Lkc "${flash_path_to_backup_restore[@]}" "${var_path_to_backup_restore[@]}")
	backup_size_k_all=$(echo "$out_du_Lkc_backup" | tail -n1 | awk '{print $1}')
	overall_backup_size=$(( backup_size_k + backup_size_k_all )) || error "Failed to compute backup size"

	if [ $overall_backup_size -gt $MAX_ALLOWED_BACKUP_SIZE_K ]; then
		info "Failed"
		space_to_clean_mb=$(( 1 + (overall_backup_size - MAX_ALLOWED_BACKUP_SIZE_K) / 1024 ))
		warn "overall backup size is crossing max limit by $space_to_clean_mb MB."
		info "---------------------------------------------------------------"
		info "Components to review manually (du -sLh <path>):"
		du -sLh "${DATA2RETAIN[@]}" | sort -h
		info "---------------------------------------------------------------"
		error "Please manually delete atleast $space_to_clean_mb MB in total from listed paths"
	fi
	info "Done"
}

_l2f_swapoff_next_flash() {
	if [[ -e "$swap_next_flash" ]] && [[ $(swapon --summary | grep -c $swap_next_flash) == 1 ]]; then
		info_step "Disable swap on $swap_next_flash"
		swapoff "$swap_next_flash" && info "Done" && return
		info "Swap in use"

		if [[ $running_linux_ns == 1 ]]; then
			_reboot
		fi
	fi
}

_l2f_flash_extend() {
	if [[ -e "$swap_next_flash" ]] && [[ $(swapon --summary | grep -c $swap_next_flash) == 0 ]]; then
		debug "swap partition $swap_next_flash not in use"
		info_step "Delete swap partition"
		partnbr_swap=${swap_next_flash: -1}
		run 'parted -s /dev/"$disk0" rm "$partnbr_swap"' || (info "Reboot required" && _reboot)
		info "Done"
		out_parted_free_disk0=$(parted -m /dev/"$disk0" "unit kiB print free")
		out_parted_part_after_flash=$(echo "$out_parted_free_disk0" | grep -A1 ":ext4:" | grep -A1 "^$partnbr_flash:" | tail -n1)
	fi

	if [[ $(echo "$out_parted_part_after_flash" | grep -c ":free;") == 1 ]]; then
		debug "free space exist after flash partition"
		info_step "Extend flash partition $dev_flash"
		end_kib=$(echo "$out_parted_part_after_flash" | awk -F: '{print $3}')
		run 'echo yes | parted /dev/"$disk0" ---pretend-input-tty resizepart "$partnbr_flash" "$end_kib"' \
			|| warn "Failed to extend /flash partition"
		info "Done"
	fi

	run 'resize2fs "$dev_flash"' || warn "Filesystem resize failed for /flash partition"
}

_l2f_installer_backup_to_flash() {
	#copy installer files to flash partition
	[[ "$install_dirpath" =~ ^/flash(/|$) ]] && { echo "Already in /flash, skipping copy."; return 0; }

	info_step "Backup installer files to flash"
	flash_install_dirpath="/flash$install_dirpath"
	mkdir -p "$flash_install_dirpath"
	run 'cp -avf "${install_dirpath}"/* "$flash_install_dirpath"/' || error "failed to copy installer files"
	info "Done"
}

_l2f_var_clean_before_shrink() {
	if [[ "$install_dirpath_original" == "/var/"* ]]; then
		rm -rf "$install_dirpath_original"
	fi
}

_l2f_shrink_var_n_create_tmp_partition() {
	dev_var=$(blkid -L "$var_partition_label")
	partnbr_var=${dev_var: -1}

	out_fdisk_l=$(fdisk -l /dev/"$disk0")
	partinfo_after_var=$(echo "$out_fdisk_l" | grep -A9 "^$dev_var " | awk '{print $1}')
	dev_tmp_fat_part=$(echo "$partinfo_after_var" | tail -n1 | awk '{print $1}')

	out_parted_free_disk0=$(parted -m /dev/"$disk0" "unit kiB print free")
	tmp_fat_partition_count=$(echo "$out_parted_free_disk0" | grep -v ":free;" | tail -n2 | grep -c ":fat32:")

	if [[ -z "$1" ]] && [[ $tmp_fat_partition_count == 1 ]]; then
		_mount_l2f_backup_partitions || __l2f_format_tmp_partition
		return
	elif [[ -z "$1" ]]; then
		# var is supposed to be last partition, delete any undesired partitions present after it.
		for dev in $partinfo_after_var; do
			[[ "$dev" == "$dev_var" ]] && continue
			partnbr=${dev: -1}
			run 'parted -s /dev/"$disk0" rm "$partnbr"' || warn "failed to remove partition(s) after var"
		done
	elif ([[ "$1" == "backup2" ]] && [[ $tmp_fat_partition_count == 2 ]]); then
		_mount_l2f_backup_partitions || __l2f_format_tmp_partition
		return
	fi

	info_step "Shrink var partition"
	sync
	_unmount_var_partition
	run 'e2fsck -fp "$dev_var"; resize2fs -fM "$dev_var"; resize2fs -fM "$dev_var"'

	out_dumpe2fs_h_var=$(dumpe2fs -h "$dev_var")
	var_block_count_new=$(echo "$out_dumpe2fs_h_var" | grep "^Block count:" | awk '{print $NF}')
	var_block_size=$(echo "$out_dumpe2fs_h_var" | grep "^Block size:" | awk '{print $NF}')
	var_new_size_k=$(( (var_block_count_new * var_block_size) / 1024 ))

	out_parted_var=$(echo "$out_parted_free_disk0" | grep -v ":free;" | grep "^$partnbr_var:" | tail -n1)
	out_parted_var_start_k=$(echo "$out_parted_var" | awk -F: '{print $2}')
	out_parted_var_start_k=${out_parted_var_start_k%kiB}
	out_parted_var_end_k=$(echo "$out_parted_var" | awk -F: '{print $3}')
	out_parted_var_end_k=${out_parted_var_end_k%kiB}

	new_parted_var_end_k=$(( out_parted_var_start_k + var_new_size_k + PART_SHRINK_SIZE_BUFFER_K ))
	min_parted_var_end_k=$(( out_parted_var_end_k - L2F_BACKUP_PART_MAXSIZE_K - PART_SHRINK_SIZE_BUFFER_K ))

	if [[ $min_parted_var_end_k -gt $new_parted_var_end_k ]]; then
		new_parted_var_end_k=$min_parted_var_end_k
	else
		warn "backup1 partition will be smaller than expected"
	fi

	run 'echo yes | parted /dev/"$disk0" ---pretend-input-tty resizepart "$partnbr_var" "$new_parted_var_end_k"kiB' \
		|| warn "Failed to shrink /var partition"
	run 'e2fsck -fp "$dev_var"' || warn "Filesystem corrupted on /var partition"
	_mount_var_partition
	info "Done"

	tmp_part_start_k=$(( new_parted_var_end_k + PART_SHRINK_SIZE_BUFFER_K ))
	info_step "Create data backup partition"
	debug "Creating FAT32 partition from ${tmp_part_start_k}kiB to end..."
	run 'echo yes | parted /dev/"$disk0" ---pretend-input-tty mkpart logical fat32 "$tmp_part_start_k"kiB 100%' \
		|| error "Failed to create tmp partition"
	info "Done"

	__l2f_format_tmp_partition
}

__l2f_format_tmp_partition() {
	out_fdisk_l=$(fdisk -l /dev/"$disk0")
	partinfo_after_var=$(echo "$out_fdisk_l" | grep -A2 "^$dev_var " | grep " FAT32 ")
	dev_tmp_fat_part=$(echo "$partinfo_after_var" | tail -n1 | awk '{print $1}')

	info_step "Format data backup partition"
	run 'mkfs.vfat -F 32 "$dev_tmp_fat_part"' || error "Failed to format tmp partition"
	run 'fsck -yf "$dev_tmp_fat_part"' || error "faulty filesystem on backup partition"
	info "Done"
}

_mount_l2f_backup_partitions() {
	info_step "Mount backup partition(s)"
	out_fdisk_l=$(fdisk -l /dev/"$disk0")
	partinfo_after_extended=$(echo "$out_fdisk_l" | grep -A9 "^$dev_extended " | grep " FAT32 ")

	dev_l2f_backup1_part=$(echo "$partinfo_after_extended" | head -n1 | awk '{print $1}')
	if [[ -e "$dev_l2f_backup1_part" ]] && [[ $(mount | grep -c "^$dev_l2f_backup1_part ") != 1 ]]; then
		mkdir -p "$mp_l2f_backup1"
		mount -t vfat "$dev_l2f_backup1_part" "$mp_l2f_backup1"
	fi
	if [[ $(mount | grep -c " $mp_l2f_backup1 ") != 1 ]]; then
		return 2
	fi

	dev_l2f_backup2_part=$(echo "$partinfo_after_extended" | grep -v "$dev_l2f_backup1_part" | head -n1 | awk '{print $1}')
	if [[ -e "$dev_l2f_backup2_part" ]] && [[ $(mount | grep -c "^$dev_l2f_backup2_part ") != 1 ]]; then
		mkdir -p "$mp_l2f_backup2"
		mount -t vfat "$dev_l2f_backup2_part" "$mp_l2f_backup2"
		if [[ $(mount | grep -c " $mp_l2f_backup2 ") != 1 ]]; then
			return 1
		fi
	fi

	echo "$out_fdisk_l" > "/flash/l2f_fdisk_disk0_table"
	info "Done"
}

_unmount_l2f_backup_partitions() {
	info_step "Unmount backup partition(s)"
	run 'umount -f "$mp_l2f_backup1"'
	run 'umount -f "$mp_l2f_backup2"'
	info "Done"
}

__backup_path1_to_path2() {
	# $1 : file or dir path
	# $2 : path under which create backup
	local file="$1"
	local backup_path="$2"
	local parent_dir=$(dirname "$file")
	if [ -d "$file" ]; then
		rm -f "$backup_path/$file$tmp_suffix"_dir "$backup_path/$file$backup_suffix"_dir
		mkdir -p "$backup_path/$parent_dir"
		filename=$(basename "$file")
		run 'cd "$parent_dir" && tar cvfz "$backup_path/$file$tmp_suffix"_dir "$filename"' \
			&& run 'mv "$backup_path/$file$tmp_suffix"_dir "$backup_path/$file$backup_suffix"_dir' \
			&& return 0 \
			|| (rm -f "$backup_path/$file$tmp_suffix"_dir "$backup_path/$file$backup_suffix"_dir; return 1)
	elif [ -e "$file" ]; then
		rm -f "$backup_path/$file$tmp_suffix" "$backup_path/$file$backup_suffix"
		mkdir -p "$backup_path/$parent_dir"
		run 'cp -vf "$file" "$backup_path/$file$tmp_suffix"' \
			&& run 'mv "$backup_path/$file$tmp_suffix" "$backup_path/$file$backup_suffix"' \
			&& return 0 \
			|| (rm -f "$backup_path/$file$tmp_suffix" "$backup_path/$file$backup_suffix"; return 2)
	else
		debug "missing $file, ignoring."
	fi
	return 3
}

__restore_path1_from_path2() {
	# $1 : file or dir path
	# $2 : path under which backup is present
	local file="$1"
	local backup_path="$2"
	local parent_dir=$(dirname "$file")
	if [ -f "$backup_path/$file$backup_suffix"_dir ]; then
		mkdir -p "$parent_dir"
		filename=$(basename "$file")
		run 'cd "$parent_dir" && tar xvfz "$backup_path/$file$backup_suffix"_dir' \
			&& (rm -f "$backup_path/$file$backup_suffix"_dir; return 0) \
			|| return 1
	elif [ -f "$backup_path/$file$backup_suffix" ]; then
		mkdir -p "$parent_dir"
		run 'cp -vf "$backup_path/$file$backup_suffix" "$file"' \
			&& (rm -f "$backup_path/$file$backup_suffix"; return 0) \
			|| return 1
	fi
}

_l2f_backup_to_tmp_step1() {
	local backup_available_k=$(eval $backup_free_k_cmd)
	mkdir -p "$backup_path"
	for fpath in "${l2f_f2b[@]}"; do
		[[ ! -e "$fpath" ]] && l2f_f2b_done+=("$fpath") && continue
		local fsize_k=$(du -sLkc "$fpath" | tail -n1 | awk '{print $1}')
		debug "file= '$fpath', fsize = $fsize_k, available = $backup_available_k"
		if [ $(( fsize_k + MIN_FREE_K )) -lt $backup_available_k ]; then
			__backup_path1_to_path2 "$fpath" "$backup_path"
			if [[ $? == 0 ]]; then
				l2f_f2b_done+=("$fpath")
				rm -rf "$fpath"
				sync
				backup_available_k=$(eval $backup_free_k_cmd)
			else
				l2f_f2b_revisit+=("$fpath")
			fi
		else
			l2f_f2b_revisit+=("$fpath")
		fi
	done
}

_l2f_backup_to_tmp_step2() {
	local backup_available_k=$(eval $backup_free_k_cmd)
	mkdir -p "$backup_path_splits"
	for fpath in "${l2f_f2b_revisit[@]}"; do
		[[ "$fpath"* == "$l2f_backup_split_tmp" ]] && continue

		local fbasename=$(basename "$fpath")
		local parent_dir=$(dirname "$fpath")
		local fsize_k=$(du -sLkc "$fpath" | tail -n1 | awk '{print $1}')
		debug "file= '$fpath', fsize = $fsize_k, available = $backup_available_k"

		rm -rf "$l2f_backup_split_tmp"
		mkdir -p "$l2f_backup_split_tmp/$parent_dir"
		run 'cd "$parent_dir" && tar cvfz - "$fbasename" | split -d -b ${MIN_FREE_K}K - "$l2f_backup_split_tmp/$fpath"_tgz_' \
			&& (l2f_f2b_done+=("$fpath"); rm -rf "$fpath") \
			|| rm -rf "$l2f_backup_split_tmp"

		for sfp in "$l2f_backup_split_tmp/$parent_dir/"*; do
			[ $(( MIN_FREE_K + MIN_FREE_K )) -lt $backup_available_k ] || return 0
			local bsfp="$backup_path_splits/${sfp#$l2f_backup_split_tmp}"
			local bsfp_dir=$(dirname "$bsfp")
			mkdir -p "$bsfp_dir"
			run 'cp -vf "$sfp" "$bsfp"' \
				|| (rm -f "$bsfp"; return 0)
			rm -f "$sfp"
			sync
			backup_available_k=$(eval $backup_free_k_cmd)
		done
	done
}

_l2f_backup_to_tmp_step3() {
	# Try backing up flash data files as well before changing mountpoint
	l2f_f2b=("${flash_path_to_backup_restore[@]}")
	_l2f_backup_to_tmp_step1
}

_l2f_backup_to_tmp_step4() {
	backup_mp="$mp_l2f_backup2"
	backup_path="$backup_mp/$l2f_backup_dir"
	backup_path_splits="$backup_mp/splits_${l2f_backup_dir}"
	backup_free_k_cmd="df -k \"$backup_mp\" | grep '$backup_mp' | awk '{print \$4}'"

	mkdir -p "$backup_path" "$backup_path_splits"
	run 'cp -rvf "$l2f_backup_split_tmp/"* "$backup_path_splits/"'
	rm -rf "$l2f_backup_split_tmp"
}

_l2f_backup_to_tmp_step5() {
	l2f_f2b=("${l2f_f2b_revisit[@]}")
	l2f_f2b_revisit=
	for fdel in "${l2f_f2b_done[@]}"; do
		l2f_f2b=("${l2f_f2b[@]/$fdel}")
	done
	_l2f_backup_to_tmp_step1

	[[ -z "$l2f_f2b_revisit" ]] || error "we ran out of space, some files may go missing"
}

_l2f_backup_to_tmp_partition() {
	l2f_backup_split_tmp="/flash/tmp/l2f_splits"
	backup_mp="$mp_l2f_backup1"
	backup_path="$backup_mp/$l2f_backup_dir"
	backup_path_splits="$backup_mp/splits_${l2f_backup_dir}"
	backup_free_k_cmd="df -k \"$backup_mp\" | grep '$backup_mp' | awk '{print \$4}'"
	l2f_f2b=("${var_path_to_backup_restore[@]}")
	l2f_f2b_revisit=()
	l2f_f2b_done=()

	info_step "Backup (1/5): backup var partition data"
	_l2f_backup_to_tmp_step1 && info "."

	info_step "Backup (2/5): split huge files"
	_l2f_backup_to_tmp_step2 && info "."

	info_step "Backup (3/5): backup flash partition data"
	_l2f_backup_to_tmp_step3 && info "."

	# Shrink again and create tmp2
	_l2f_shrink_var_n_create_tmp_partition "backup2"
	_mount_l2f_backup_partitions || error "failed to mount backup partitions"

	info_step "Backup (4/5): backup remaining splits"
	_l2f_backup_to_tmp_step4 && info "."

	info_step "Backup (5/5): backup any remaining data"
	_l2f_backup_to_tmp_step5
	info "Done"
}

_l2f_shrink_extended_partition() {
	disk0_partition_info=$(cat "$fdisk_data_file")
	partinfo_after_extended=$(echo "$out_fdisk_l" | grep -A9 "^$dev_extended " | grep " FAT32 ")

	dev_l2f_backup1_part=$(echo "$partinfo_after_extended" | head -n1 | awk '{print $1}')

	backup1_part_info=$(echo "$disk0_partition_info" | grep "^$dev_l2f_backup1_part ")
	backup1_part_start=$(echo "$backup1_part_info" | awk '{print $2}')
	backup1_part_end=$(echo "$backup1_part_info" | awk '{print $3}')
	backup1_part_id=$(echo "$backup1_part_info" | awk '{print $6}')

	backup2_part_info=$(echo "$disk0_partition_info" | grep "^$dev_l2f_backup2_part ")
	backup2_part_start=$(echo "$backup2_part_info" | awk '{print $2}')
	backup2_part_end=$(echo "$backup2_part_info" | awk '{print $3}')
	backup2_part_id=$(echo "$backup2_part_info" | awk '{print $6}')

	new_extended_start=$(( backup2_part_start - 4096 ))

	sync
	_unmount_var_partition
	_unmount_l2f_backup_partitions

	info_step "Shrink extended partition"
	# delete extended partition
	run '( echo "d"; echo "$partnbr_extended"; echo "w" ) | fdisk /dev/"$disk0"'
	# re-create extended partition
	run '( echo "c"; echo "n"; echo "e"; echo "$partnbr_extended"; \
		echo "$new_extended_start"; echo ""; echo "w" ) | fdisk /dev/"$disk0"'
	# re-create backup2 partition with type fat32
	run '( echo "c"; echo "n"; echo "l"; echo "$backup2_part_start"; \
		echo "$backup2_part_end"; echo "N"; echo "t"; echo "5"; \
		echo "$backup2_part_id"; echo "w" ) | fdisk /dev/"$disk0"'
	# re-create backup2 partition with type fat32
	run '( echo "c"; echo "n"; echo "l"; echo "$backup1_part_start"; \
		echo "$backup1_part_end"; echo "N"; echo "t"; echo "6"; \
		echo "$backup1_part_id"; echo "w" ) | fdisk /dev/"$disk0"'
	run 'fsck -yf /dev/"$disk0"5'
	run 'fsck -yf /dev/"$disk0"6'
	_mount_l2f_backup_partitions || error "failed to mount backup partitions"
	info "Done"
}

_l2f_backup_to_tmp_installer_files() {
	backup_install_dirpath="$mp_l2f_backup1/nsinstall"
	backup_install_tgz="$backup_install_dirpath/ns-installer_tgz"

	mkdir -p "$backup_install_dirpath"
	_switch_logging_to_path "$backup_install_dirpath/$LOGFILENAME"
	[ -f "$backup_install_tgz" ] && return

	info_step "Backup installer files"
	rm -f "$backup_install_tgz$tmp_suffix" "$backup_install_tgz"
	run 'cd "$install_dirpath" && tar cvfz "$backup_install_tgz$tmp_suffix" *' || error "Failed to create installer archive"
	run 'mv "$backup_install_tgz$tmp_suffix" "$backup_install_tgz$backup_suffix"' || error "Failed to rename installer archive"

	run 'cp -fv "$install_dirpath/$kernel_name"* "$backup_install_dirpath/"' || error "Failed to copy freebsd kernel to tmp2 partition"
	run 'cp -fv "$installer" "$backup_install_dirpath/"' || error "Failed to copy standalone installns script to tmp2 partition"
	info "Done"
}

_l2f_install_grub_to_backup2() {
	info_step "Install grub"
	l2f_backup_boot="$mp_l2f_backup1/boot"
	l2f_backup_grub_dir="$l2f_backup_boot/grub"
	l2f_backup_grub_cfg="$l2f_backup_grub_dir/grub.cfg"
	partnbr_backup1=${dev_l2f_backup1_part: -1}

	mkdir -p "$l2f_backup_grub_dir"
	cat > "$l2f_backup_grub_cfg" <<EOF
set default="0"
set timeout="0"

menuentry "Resume NetScaler Upgrade (build $NSVERSION)" {
	kfreebsd "(hd0,msdos$partnbr_backup1)/nsinstall/$kernel_name"
}
EOF
	run '/sbin/grub-install --boot-directory="$l2f_backup_boot" --target=i386-pc /dev/"$disk0"' \
		|| warn "GRUB installation failed on $disk0"
	run 'parted -s /dev/"$disk0" set $partnbr_backup1 boot on' || warn "Failed to set boot flag"
	info "Done"
}

_l2f_restore_freebsd_partitions() {
	local disk0_parts=$(gpart show -p "$disk0" | grep -v "^=>")
	local extended_part=$(echo "$disk0_parts" | grep "ebr" | awk '{print $3}')
	local extended_part_info=$(gpart show -p "$extended_part")
	local backup1_part=$(echo "$extended_part_info" | grep " fat32lba " | grep " \[active\] " | head -n1 | awk '{print $3}')
	local backup2_part=$(echo "$extended_part_info" | grep " fat32lba " | grep -v " \[active\] " | head -n1 | awk '{print $3}')

	([ -z "$backup1_part" ] || [ -z "$backup2_part" ]) && error "failed to find backup partitions"

	# we expecte backup1_part already mounted and this script is running from it.
	# update backup mountpoint(s) accordingly
	cur_mp_l2f_backup1=$(mount -p | grep "$backup1_part" | awk '{print $2}')
	[ -n "$cur_mp_l2f_backup1" ] && mp_l2f_backup1="$cur_mp_l2f_backup1"
	[ "$mp_l2f_backup1" == "$mp_l2f_backup2" ] && mp_l2f_backup2="$cur_mp_l2f_backup1"_2

	dev_l2f_backup1_part=/dev/"$backup1_part"
	dev_l2f_backup2_part=/dev/"$backup2_part"

	info_step "Remove obsolete partitions"
	local disk0_parts_to_remove_info=$(echo "$disk0_parts" | grep -v " \- free \- " | grep -v " $extended_part " \
					| grep -v " freebsd " | awk '{print $3}')
	while read part; do
		[ -z "$part" ] && continue
		partnbr=${part: -1}
		run 'gpart delete -i $partnbr $disk0' || warn "failed to delete partition '$disk0'->'$partnbr'"
	done <<< "$disk0_parts_to_remove_info"
	info "Done"

	info_step "Create missing freebsd slice, partitions"
	info ""

	if [ $(gpart show -p "$disk0" | grep -c " freebsd ") == 0 ]; then
		run 'gpart add -t freebsd -i 1 "$disk0"' \
			&& info "- added BSD partition" \
			|| warn "failed to add freebsd partition"
	fi

	geom0="$disk0"s1
	if [ $(gpart show -p "$geom0" 2>/dev/null | grep -c " $geom0 ") == 0 ]; then
		run 'gpart create -s BSD "$geom0"' \
			&& info "- converted BSD partition to slice" \
			|| warn "failed to create BSD slice"
	fi

	dev_flash=/dev/"$geom0"a
	dev_swap=/dev/"$geom0"b
	dev_d=/dev/"$geom0"d
	dev_var=/dev/"$geom0"e

	partnbr_flash=1
	partnbr_swap=2
	partnbr_d=4
	partnbr_var=5

	if [ ! -e $dev_flash ]; then
		run 'gpart add -t freebsd-ufs -b 0 -s 3354624 -i $partnbr_flash $geom0' \
			&& info "- created flash partition" \
			|| warn "failed to create flash partition"
		run '/sbin/newfs $dev_flash'
	fi

	if [ ! -e $dev_swap ]; then
		run 'gpart add -t freebsd-swap -b 3354624 -s 8597504 -i $partnbr_swap $geom0' \
			&& info "- created swap partition" \
			|| warn "failed to create swap partition"
	fi

	if [ ! -e $dev_d ]; then
		run 'gpart add -t freebsd-ufs -b 11952128 -s 4096 -i $partnbr_d $geom0' \
			&& info "- created d partition" \
			|| warn "failed to create d partition"
	fi

	if [ ! -e $dev_var ]; then
		run 'gpart add -t freebsd-ufs -b 11956224 -i $partnbr_var $geom0' \
			&& info "- created var partition" \
			|| warn "failed to create var partition"
		run '/sbin/newfs $dev_var'
	fi
	info "Done"
}

__mount_system_partitions_freebsd() {
	mkdir -p "$flash_dir" "$var_dir"
	run 'fsck -fy "$dev_flash"'
	run 'fsck -fy "$dev_var"'
	run 'mount -f "$dev_flash" "$flash_dir"'
	run 'mount -f "$dev_var" "$var_dir"'
}

__mount_backup_partitions_freebsd() {
	mkdir -p "$mp_l2f_backup1" "$mp_l2f_backup2"
	run 'fsck -fy "$dev_l2f_backup1_part"'
	run 'fsck -fy "$dev_l2f_backup2_part"'
	run 'mount -t msdosfs "$dev_l2f_backup1_part" "$mp_l2f_backup1"'
	run 'mount -t msdosfs "$dev_l2f_backup2_part" "$mp_l2f_backup2"'
}

__umount_backup_partitions_freebsd() {
	run 'umount -f "$mp_l2f_backup1"'
	run 'umount -f "$mp_l2f_backup2"'
}

_l2f_restore_installer_files() {
	info_step "Restore installer files"
	mkdir -p "$install_dirpath_original"
	backup_install_tgz="$install_dirpath/ns-installer_tgz"
	if [ -f "$backup_install_tgz$backup_suffix" ]; then
		run 'cd "$install_dirpath_original" && tar xvfz "$backup_install_tgz$backup_suffix"' || error "Failed to restore installer files"
		rm -f "$backup_install_tgz$backup_suffix"
	fi

	install_dirpath="$install_dirpath_original"
	_switch_logging_to_path "$install_dirpath/$LOGFILENAME"
	_write_resume_file "$DEFAULT_RESUME_FILEPATH"
	info "Done"
}

_l2f_restore_data_f2r() {
	l2f_splits_dir="splits_${l2f_backup_dir}"
	l2f_splits_tmpdir="/var/l2f_tmp"

	info_step "Restore (1/2): backup from splits"
	# Restore splits first
	if [ -n "$(ls -A "$mp_l2f_backup2/$l2f_splits_dir" 2>/dev/null)" ] \
		|| [ -n "$(ls -A "$mp_l2f_backup1/$l2f_splits_dir" 2>/dev/null)" ]; then
		rm -rf "$l2f_splits_tmpdir"
		for fpath in "${l2f_f2r[@]}"; do
			pdir=$(dirname "$fpath")
			tmp_pdir="$l2f_splits_tmpdir/$pdir"
			fname=$(basename "$fpath")
			mkdir -p "$tmp_pdir"
			for backup_mp in "$mp_l2f_backup2" "$mp_l2f_backup1"; do
				if [ -n "$(ls -A "$backup_mp/$l2f_splits_dir/$fpath"_tgz_* 2>/dev/null)" ]; then
					for sfp in "$mp_l2f_backup2/$l2f_splits_dir/$fpath"_tgz_*; do
						cat "$sfp" >> "$tmp_pdir/$fname".tgz
					done
				fi
			done
			sync
			if [ -f "$tmp_pdir/$fname".tgz ]; then
				mkdir -p "$pdir"
				run 'cd "$pdir" && tar xvfz "$tmp_pdir/$fname".tgz' \
					&& rm -rf "$tmp_pdir/$fname".tgz \
					|| rm -rf "$fpath"
			fi
		done
		rm -rf "$mp_l2f_backup2/$l2f_splits_dir" "$mp_l2f_backup1/$l2f_splits_dir"
	fi
	info "Done"

	info_step "Restore (2/2): full backups"
	for fpath in "${l2f_f2r[@]}"; do
		for backup_mp in "$mp_l2f_backup2" "$mp_l2f_backup1"; do
			__restore_path1_from_path2 "$fpath" "$backup_mp/$l2f_backup_dir"
		done
	done
	info "Done"
}

_l2f_install_bsd_bootloader() {
	info_step "Install bootloader"
	bootloader="$install_dirpath/bootloader.tgz"
	if [ -f "$bootloader" ]; then
		btmp="$tmpdir/bootloader_$NSVERSION"
		rm -rf "$btmp"
		mkdir -p "$btmp"
		run 'cd "$btmp" && tar xvfz "$bootloader"'
		run 'gpart bootcode -b "$btmp/boot" "$disk0"s1'
		run 'gpart bootcode -b "$btmp/boot0" "$disk0"'
		rm -rf "$btmp"
	else
		error "bootloader files missing from installer"
	fi
	info "Done"
}

_l2f_install_freebsd_netscaler() {
	info_step "Mount all partitions"
	__mount_backup_partitions_freebsd
	__mount_system_partitions_freebsd
	info "Done"

	_l2f_restore_installer_files

	mkdir -p /flash/nsconfig /flash/boot
	cd /var && mkdir -p cron core crash log nsinstall nslog nstmp nstrace run tmp

	l2f_f2r=("${flash_path_to_backup_restore[@]}")
	_l2f_restore_data_f2r

	ln -sf /flash/nsconfig /nsconfig

	info_step "Install perl"
	run 'tar xvfz "$install_dirpath/perl5.tar" -C /'
	info "Done"

	info_step "Install NetScaler"
	cd "$install_dirpath" && \
		NSVERSION=$NSVERSION NSVARIANT=$NSVARIANT CLVERSION=$CLVERSION \
		./installns_legacy -nY | tee -a "$logfile" \
		|| error "Failed to install NetScaler"
	info "Done"

	_l2f_install_bsd_bootloader

	l2f_f2r=("${var_path_to_backup_restore[@]}")
	_l2f_restore_data_f2r
}

_l2f_remove_backup_partitions() {
	__umount_backup_partitions_freebsd
	local disk0_parts=$(gpart show -p "$disk0" | grep -v "^=>")
	local extended_part=$(echo "$disk0_parts" | grep "ebr" | awk '{print $3}')

	if [ -n "$extended_part" ] && [ -e /dev/"$extended_part" ]; then
		partnbr_extended=${extended_part: -1}
		run 'gpart delete -i "$partnbr_extended" "$disk0"' \
			|| (warn "Failed to remove extended partition"; return)
		_reboot
	fi
}

_l2f_restore_from_joined_archive() {
	l2f_splits_tmpdir="/var/l2f_tmp"

	info_step "Uncompress restored data"
	for fpath in "${l2f_f2r[@]}"; do
		pdir=$(dirname "$fpath")
		tmp_pdir="$l2f_splits_tmpdir/$pdir"
		fname=$(basename "$fpath")
		if [ -f "$tmp_pdir/$fname".tgz ]; then
			mkdir -p "$pdir"
			run 'cd "$pdir" && tar xvfz "$tmp_pdir/$fname".tgz' \
				&& rm -rf "$tmp_pdir/$fname".tgz \
				|| error "Failed to uncompress '$tmp_pdir/$fname.tgz' to $pdir"
		fi
	done
	info "Done"
}

_fix_downgrade_issues() {
	dg_pr_dir="/netscaler/downgrade_prerequisite"
	dg_pr_main="$dg_pr_dir/main"

	if [ -f "$dg_pr_main" ]; then
		info_step "Fix version incompatibility"
		chmod +x "$dg_pr_main"
		out=$(NSVERSION=$NSVERSION NSVARIANT=$NSVARIANT CLVERSION=$CLVERSION "$dg_pr_main" -o fix 2>&1)
		rcode=$?
		debug "script output (code=$rcode): $out"

		if [ $rcode -eq 0 ]; then
			info "Done"
		else
			info "Failed (ignored, continuing upgrade)"
			warn "Fix to version incompatibility failed (code=$rcode)."
		fi
	fi
}

_reboot() {
	if [[ $running_linux_ns_installer == 1 ]]; then
		info "Reboot in 3 secs..."
		sync && sleep 3
		reboot
		exit 0
	fi

	cat <<EOF

Reboot of the appliance is required to continue the upgrade.
EOF

	if [[ $OPTION_NO_REBOOT == true ]]; then
		echo "Please perform reboot manually. rebooting now is recommended"
		echo "[Option 'n' set, installer will exit now ....]"
		exit 0
	fi

	if [[ $OPTION_FORCE_REBOOT == true ]]; then
		echo "[Option 'y' set, appliance will reboot in 5 secs ....]"
	elif [[ $OPTION_GUI_FLAG == true ]]; then
		echo "Please perform reboot manually. rebooting now is recommended"
		echo "[GUI/API mode, installer will exit now ....]"
		exit 10
	else
		cat <<EOF
press 'n' if you want to perform reboot later manually. Installer will resume
post reboot in either case.
EOF
		_confirmation_prompt "Would you like to reboot now?" "n" 0
	fi
	_disable_signal_handling
	# ADM / SVM parse console output for 'Rebooting' as part of ADC upgrade,
	# changing this log will result in breakage of backward compatibility.
	echo "Rebooting ..."
	sleep 5
	reboot
	exit 0
}

install_safenet() {
    if [[ -f "./safenet_dirs.tar" ]]; then
        if [[ -d "/var/safenet/" ]]; then
            log_status "/var/safenet/ directory exists."
            conf="/var/tmp/safenet_conf.$$tar"
            if [[ -f "/var/safenet/safenet/version" ]]; then
                # save essential configuration
                rm -f "$conf"
                tar cvf "$conf" -C /var/safenet safenet_is_enrolled sfgw_ident_file sfnt_key_boot_file gateway/gw_delay 2>> ./err_msg
                (cd /var/safenet && find config/ -name "Chrystoki*.conf" -exec tar -uf "$conf" {} \;)
                (cd /var/safenet && tar -uvf "$conf" safenet/version safenet/lunaclient/configData safenet/lunaclient/cert safenet/lunaclient/htl safenet/lunaclient/data 2>> ./err_msg)
            fi

            # wipe the /var/safenet directory
            rm -rf /var/safenet 2>> ./err_msg
            # install safenet bundle
            tar xpf safenet_dirs.tar --uid 0 --gid 0 -C /var 2>> ./err_msg
            cp -f safenet_dirs.tar /var/safenet 2>> ./err_msg
            chown root:wheel /var/safenet/safenet_dirs.tar 2>> ./err_msg

            # restore configuration
            if [[ -f "$conf" ]]; then
                tar xpvf "$conf" -C /var/safenet 2>> ./err_msg
                version=$(cat /var/safenet/safenet/version)
                tar xzvf "/var/safenet/SAClient_${version}.tgz" -C /var/safenet safenet/lunaclient/bin safenet/lunaclient/lib 2>> ./err_msg
                rm -f "$conf"
            fi
        else
            log_status "Installing safenet files..."
            tar xpf safenet_dirs.tar --uid 0 --gid 0 -C /var 2>> ./err_msg
        fi
    fi
}

# UTIL FUNCTIONS
_log2file() {
	caller_func=${FUNCNAME[1]}
	if [[ -n "$2" ]]; then
		caller_func="$2"
	fi
	echo "[$(date '+%F %T')][$$ $caller_func] $1" >> "$logfile" 2>&1
}

debug() {
	if [[ $OPTION_VERBOSE == true ]]; then
		echo "DEBUG: $*"
	fi
	_log2file "DEBUG: $*" "${FUNCNAME[1]}"
}

info() {
	if [[ $OPTION_VERBOSE == true ]]; then
		echo -n "INFO: "
	fi
	echo "$*"
	_log2file "INFO: $*" "${FUNCNAME[1]}"
}

warn() {
	echo "WARNING: $*"
	_log2file "WARNING: $*" "${FUNCNAME[1]}"
}

error() {
	echo ""
	echo "ERROR: $*, exiting installer."
	_log2file "ERROR: $*, exiting installer" "${FUNCNAME[1]}"
	if [[ $OPTION_BASH_ON_ERROR == true ]]; then
		_disable_signal_handling
		disable_logging
		echo "Launching developer shell on fail..."
		bash
	fi
	nsexit 202
}

info_step() {
	echo -n "=> $* ...."
	if [[ $OPTION_VERBOSE == true ]]; then
		echo "started $(date '+%F %T')"
	fi
	_log2file "INFO: $*" "${FUNCNAME[1]}"
}

run() {
	rc=
	re_run_cmd=false
	if [[ $OPTION_VERBOSE == true ]]; then
		(eval "$*") | tee -a "$logfile" 2>&1
		rc=${PIPESTATUS[0]}
	else
		(eval "$*") >>"$logfile" 2>&1
		rc=$?
	fi
	if [[ $re_run_cmd == true ]]; then
		debug "Rerunning cmd: $*"
		run "$*"
		rc=$?
	fi
	return $rc
}

_confirmation_prompt() {
	# _confirmation_prompt <msg> <exit_with:y|n> <exitcode>
	echo ""
	local attempts=1
	local att_str=
	while [[ $attempts -le $MAX_ATTEMPTS ]]; do
		while read -t 1 discard; do
			continue
		done
		if [[ $attempts -ge 2 ]]; then
			att_str="($attempts/$MAX_ATTEMPTS) "
		fi
		if [[ -n "$1" ]]; then
			echo -n " $1 [y/n]: $att_str"
		fi
		read -n 1 -r || error "Failed to read user input"
		echo ""
		if ([[ $REPLY =~ ^[Yy]$ ]] && [[ "$2" =~ [Yy] ]]) ||
			([[ $REPLY =~ ^[Nn]$ ]] && [[ "$2" =~ [Nn] ]]); then
			nsexit $3
		elif [[ $REPLY =~ ^[YyNn]$ ]]; then
			return
		fi
		attempts=$(( attempts+1 ))
	done
	warn "Maximum attempts reached, installer will exit now"
	exit 0
}

_th_interrupt() {
	_confirmation_prompt "Installer interrupted, do you want to stop upgrade and exit?" "y" 201
	re_run_cmd=true
}

_th_exit() {
	_clean_pidfile
	debug "Installer cleanup done"
}

_set_partnbr() {
	# arg1: variable name to which assign the partnbr value
	# arg2: immediate geom or parent slice path, for example '/dev/ada0s1' or 'ada0s1'
	# arg3: partition path or name in dev, for example '/dev/ada0s1a' or 'ada0s1a'
	# arg4: expected value by caller, log warning in case of mismatch.
	tmp2=$(echo "$2" | sed "s;/dev/;;")
	tmp3=$(echo "$3" | sed "s;/dev/;;")
	tmp2=$(echo "$tmp3" | sed "s;$tmp2;;")
	tmp3=$(echo "$tmp2" | tr a-i 1-9)
	if [[ "$tmp3" == "$tmp2" ]]; then
		tmp3=$(echo "1$tmp2" | tr j-s 0-9)
	fi
	if [[ "$tmp3" == "1$tmp2" ]]; then
		tmp3=$(echo "2$tmp2" | tr t-z 0-6)
	fi
	if [[ $4 != $tmp3 ]]; then
		warn "Expected '$tmp2' partition number as '$4', got '$tmp3', proceeding ...."
	fi
	eval $1=$tmp3
	debug "$1=$tmp3"
}

_clean_pidfile() {
	if [ -f "$PIDFILE" ]; then
		other_pid=$(cat "$PIDFILE")
		if [ "$$" == "$other_pid" ]; then
			rm -f "$PIDFILE"
		fi
	fi
}

truncate_log() {
	if [ -f "$logfile" ]; then
		logfile_size_k=$(du "$logfile" | awk '{print $1}')
		if [[ $((logfile_size_k*1024)) -gt $MAX_LOG_BYTES_ON_START ]]; then
			info "logfile size: $logfile_size_k KB"
			info_step "Truncate logfile '$logfile'"
			mv "$logfile" "$logfile.tmp"
			tail -c $TRUNCATED_LOG_BYTES "$logfile.tmp" >> "$logfile" 2>/dev/null
			rm -f "$logfile.tmp"
			info "Done"
		fi
	fi
}

_switch_logging_to_path() {
	local old_logfile=$(realpath "$logfile")
	if [[ -n "$1" ]]; then
		local new_logfile="$1"
		local pdir=$(dirname "$new_logfile")
		local logname=$(basename "$new_logfile")
		mkdir -p "$pdir"
		new_logfile="$(realpath "$pdir")/$logname"
		if [[ "$old_logfile" != "$new_logfile" ]]; then
			info_step "Move installer logfile to $new_logfile"
			debug "installer logfile -> '$new_logfile'"
			disable_logging
			cat "$logfile" >> "$new_logfile" && rm -f "$old_logfile" \
				|| warn "failed to copy logs to '$new_logfile'"
			logfile="$new_logfile"
			init_logging
			info "Done"
		else
			debug "switch logfile skipped, continue logging at $logfile"
		fi
	fi
}

init_logging() {
	exec 3>&2 2>>"$logfile"
	set -x
}

disable_logging() {
	set +x
	exec 2>&3 3>&-
}
####################
NSVERSION=14.1-60.57
NSVARIANT=v
CLVERSION=133

# initialize logging
MAX_LOG_BYTES_ON_START=$(( 5 * 1024 * 1024 ))
TRUNCATED_LOG_BYTES=$(( 4 * 1024 * 1024 ))
LOGFILENAME="ns-installer.log"
logfile="$(realpath "$(dirname "$0")")/$LOGFILENAME"
truncate_log
init_logging

PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH

# Additional data to retain (used only in case of re-partition).
# note: active installer's files are also retained and are not part of below list.
DATA2RETAIN=("/flash/nsconfig"
	"/flash/boot"
	"/flash/configdb"
	"/var/download"
	"/var/netscaler/gui"
	"/var/netscaler/logon"
	"/var/vpn"
	"/var/vpns"
	"/var/tmp"
	"/var/clusterd/nsclusterd.conf"
)

init_installer_info
# check and switch to legacy installer (f2f)
if [[ -z "$install_mode" ]] && [[ $running_freebsd_ns == 1 ]] && [[ $NSVARIANT == "v" ]]; then
	disable_logging
	switch2legacy_installer "$@"
	exit $?
fi

# installer init operations
set_installer_singleton
init_signal_handling
load_options $@
init_system_info

# upgrade operations
pre_upgrade
run_upgrade
post_upgrade
