How to Generate Support Bundle in SynetoOS 6

Written By Christian Castagna (Administrator)

Updated at December 4th, 2025

→ Applies to: SynetoOS 6.x

 

Step 1. Connect to SynetoOS appliance via SSH as admin

ssh admin@<your_ip_address_or_hostname>

 

Step 2. Get root privileges

sudo su -

 

Step 3. Edit /tmp/generate-support-bundle.sh file

vi /tmp/generate-support-bundle.sh

IMPORTANT
Make sure to copy and paste the exact lines below.

#!/usr/bin/env bash

# This script dumps logs and system information into a .tar.gz archive
# Logs are collected from various sources:
# 	- Loki (via logcli)
# 	- /var/log
# 	- systemd (via journalctl)
# 	- custom commands (e.g. `zpool history`, `df`, etc.)

################################################################################
# GLOBALS                                                                      #
################################################################################

# Prevent masking erros in pipelines
set -o pipefail

# Signal handlers
trap on_exit EXIT
trap "echo -e '\nCancelled' >&2 ; exit 130" SIGINT # Terminate script when Ctrl-C is pressed

export PATH=/usr/local/bin:$PATH # /usr/local/bin/logcli
SCRIPT_NAME=$(basename "$0")
DEFAULT_OUTPUT_DIR=/var/storage/support/bundles
DEFAULT_LOGS_DURATION=1h

################################################################################
# FUNCTIONS                                                                    #
################################################################################

function usage() {
	cat <<- EOF
		Usage:
		    $SCRIPT_NAME [OPTIONS] [DIR]

		Generate support bundle. If no DIR is given, bundle is saved to $DEFAULT_OUTPUT_DIR and any file except the
		newly generated bundle is deleted to save space.

		Logs are collected from multiple sources: Loki, systemd, /var/log and ad hoc commands like \`zpool history\`.

		Options:
		    -s, --since DURATION     collect logs for the last DURATION hours or minutes (default: ${DEFAULT_LOGS_DURATION});
		                             possible sufixes: "h" (hours), "m" (minutes); only affects Loki and systemd
		    -p, --loki-port PORT     forward PORT to port 3100 inside Loki pod (default: choose port automatically)
		    -c, --no-strip-color     do not strip color codes from collected files
		    -j, --include-journal    include binary logs from /var/log/journal in the bundle; this is usually not
		                             necessary as systemd logs are already collected as plaintext with \`journalctl\`
		    -q, --quiet              suppress non-error messages
		    -h, --help               display this help text and exit
	EOF
}

function on_exit() {
	# This function is invoked on script exit
	if [[ -n "$TEMP_DIR" ]]; then
		rm --recursive --force "$TEMP_DIR" &>/dev/null
	fi
	if [[ -n "$LOKI_PORT_FORWARD_PID" ]]; then
		kill "$LOKI_PORT_FORWARD_PID" &>/dev/null
	fi
}

function log() {
	# Print to stdout based on $QUIET flag
	if [[ -z "$QUIET" ]]; then
		echo "$@"
	fi
}

function error() {
	# Print to stderr
	echo "$@" >&2
}

function start_loki_port_forward() {
	# Forward local port to port 3100 inside Loki pod.
	# If LOKI_PORT is non-empty, use its value for local port.
	# Otherwise, a free port is selected based on a retry mechanism.
	log "Starting Loki port forward"
	local start_port=${LOKI_PORT:-57343}
	local end_port=${LOKI_PORT:-57353}
	if [[ -n "$LOKI_PORT" ]]; then
		# If user specified a port, fail gracefully when that specific port is in use
		local pid
		if pid=$(pid_using_port "$LOKI_PORT"); then
			error "Port $LOKI_PORT is in use. Either try a different port or terminate process with PID $pid."
			exit 1
		fi
	fi
	if ! retry_loki_port_forward "$start_port" "$end_port"; then
		error "Cannot connect to Loki. Check Loki pod status and/or port forwarding."
		exit 1
	fi
}

function pid_using_port() {
	# Check if network port is in use.
	# Return 0 if port is in use, non-zero otherwise.
	# If port is in use, the PID of the process using it is printed.
	#
	# Parameters:
	# $1 port to check
	local port=$1
	lsof -t -i ":$port"
}

function retry_loki_port_forward() {
	# Attempt to establish a kubectl port-forward to port 3100 inside Loki pod by trying a range of local ports.
	# Return 0 if port forward is successful *and* Loki API is ready, non-zero otherwise.
	# If successful, the env variable LOKI_ADDR is exported for logcli to use.
	#
	# Parameters:
	# $1 start port
	# $2 end port
	local start_port=$1
	local end_port=$2
	local port=$start_port
	local loki_addr
	while ((port <= end_port)); do
		if pid_using_port "$port" &>/dev/null; then
			((port++))
			continue
		fi
		{ kubectl port-forward svc/loki -n monitoring "$port:3100" &>/dev/null & } &>/dev/null
		LOKI_PORT_FORWARD_PID=$!
		loki_addr="http://127.0.0.1:$port"
		if wait_loki "$loki_addr"; then
			export LOKI_ADDR=$loki_addr
			return 0
		fi
		kill "$LOKI_PORT_FORWARD_PID" &>/dev/null
		((port++))
	done
	return 1
}

function wait_loki() {
	# Check Loki API readiness
	#
	# Parameters:
	# $1 Loki address
	local loki_addr=$1
	local max_attempts=3
	local attempt=0
	while ! curl --silent "$loki_addr/ready" | grep --quiet "ready"; do
		((attempt++))
		if ((attempt == max_attempts)); then
			return 1
		fi
		sleep 1
	done
}

function collect_loki_logs() {
	# Collect logs from Loki and save them into $TEMP_DIR/loki
	local output_dir=$TEMP_DIR/loki
	local duration=${LOGS_DURATION:-$DEFAULT_LOGS_DURATION}
	local query_args=(--quiet --no-labels --forward --limit=0 --since="$duration")
	log "Collecting Loki logs (duration: $duration)"
	mkdir "$output_dir"
	for label_type in "job" "unit" "filename"; do
		# Loop for each item in the label type list
		while read -r item; do
			query="{$label_type=\"$item\"}"
			if [[ "$label_type" == "filename" ]]; then
				logname=esxi-logs
			else
				logname=$(echo "$item" | cut --delimiter '/' --fields 2)
			fi
			# shellcheck disable=SC2086
			logcli query "${query_args[@]}" "$query" >"$output_dir/$logname.log"
		done < <(logcli labels --quiet "$label_type")
	done
}

function collect_var_log() {
	# Collect logs from /var/log and save them into $TEMP_DIR/varlog
	log "Collecting /var/log"
	local output_dir=$TEMP_DIR/varlog
	local rsync_opts=(--quiet --recursive --times --prune-empty-dirs)
	if [[ -z "$INCLUDE_JOURNAL" ]]; then
		rsync_opts+=(--exclude /journal)
	fi
	mkdir "$output_dir"
	rsync "${rsync_opts[@]}" /var/log/ "$output_dir"
}

function log_command() {
	# Execute command and write its output to a file
	#
	# Parameters:
	# $1 command to run
	# $1 output file
	local command=$1
	local output_file=$2
	echo "Output of: $command" >"$output_file"
	echo >>"$output_file"
	eval "$command" >>"$output_file" 2>&1
}

function collect_commands_output() {
	# Collect outputs of various commands and save them into $TEMP_DIR/commands
	log "Collecting commands output"
	local output_dir=$TEMP_DIR/commands
	mkdir "$output_dir"
	# Note: filenames must be unique inside output dir
	log_command "config support show"                           "$output_dir/config-support.txt"
	log_command "config firmware show"                          "$output_dir/config-firmware.txt"
	log_command "systemctl"                                     "$output_dir/systemctl.txt"
	log_command "free --mebi"                                   "$output_dir/free.txt"
	log_command "lsblk --json --output-all"                     "$output_dir/lsblk.txt"
	log_command "iostat"                                        "$output_dir/iostat.txt"
	log_command "zpool list"                                    "$output_dir/zpool-list.txt"
	log_command "zpool status"                                  "$output_dir/zpool-status.txt"
	log_command "zdb | grep --extended-regexp 'ashift|\s+name'" "$output_dir/zdb-ashift.txt"
	log_command "zpool history"                                 "$output_dir/zpool-history.txt"
	log_command "zfs list"                                      "$output_dir/zfs-list.txt"
	log_command "df -h"                                         "$output_dir/df.txt"
	log_command "config net dev show"                           "$output_dir/config-net-dev.txt"
	log_command "top -b -n1"                                    "$output_dir/top.txt"
	log_command "arc_summary"                                   "$output_dir/arc-summary.txt"
}

function create_archive() {
	# Create .tar.gz archive containing all collected files found in $TEMP_DIR.
	# If output directory is default, also perform a cleanup so as to delete all files except the newly created archive.
	log "Creating archive"
	local output_dir=${OUTPUT_DIR:-$DEFAULT_OUTPUT_DIR}
	local serial_number
	local archive_path
	serial_number=$(config firmware show | grep --ignore-case serial | awk --field-separator ':' '{print $2}' | xargs | sed 's/ /-/g')
	archive_path=$output_dir/support-bundle-$(date +%Y-%m-%d_%H-%M-%S)-$serial_number.tar.gz
	mkdir -p "$(dirname "$archive_path")"
	# We use --directory to suppress error 'tar: Removing leading `/' from member names'
	tar --create --use-compress-program=pigz --file "$archive_path" --directory "$TEMP_DIR" .
	if [[ -z "$OUTPUT_DIR" ]]; then
		# We do a cleanup only when using the default output dir
		log "Cleaning up $output_dir"
		find "$output_dir" -mindepth 1 ! -name "$(basename "$archive_path")" -delete
	fi
	log "Archive created: $(realpath "$archive_path")"
}

function strip_color_codes() {
	# Strip ANSI color codes from all files found in $TEMP_DIR
	log "Stripping color codes"
	local exp='s/\x1b\[[0-9;]*[mGKHF]//g' # https://superuser.com/a/380778
	find "$TEMP_DIR" -type f -exec sed --in-place "$exp" '{}' +
}

function collect_journalctl_logs() {
	# Collect logs as generated by journalctl
	local duration=${LOGS_DURATION:-$DEFAULT_LOGS_DURATION}
	local journalctl_args=(--output=with-unit --since="-$duration")
	log "Collecting systemd logs (duration: $duration)"
	journalctl "${journalctl_args[@]}" >"$TEMP_DIR/journalctl.log"
}

function is_valid_duration() {
	# Validate duration value to be compatible with both logcli and journalctl
	# Acceptes suffixes: "h" (hours), "m" (minutes)
	#
	# Parameters:
	# $1 duration value (e.g. 24h)
	[[ "$1" =~ ^[1-9]+[0-9]*(h|m)$ ]]
}

################################################################################
# MAIN                                                                         #
################################################################################

# Check root user
if [[ "$EUID" -ne 0 ]]; then
	error "This script must be run as root"
	exit 1
fi

# Parse options
if ! OPTIONS=$(getopt -o 'hqs:cjp:' --long 'help,quiet,since:,no-strip-color,include-journal,loki-port:' --name "$SCRIPT_NAME" -- "$@"); then
	error "Error while parsing script options"
	exit 1
fi
eval set -- "$OPTIONS"
while true; do
	case "$1" in
		'-h'|'--help')
			usage
			exit
		;;
		'-s'|'--since')
			if ! is_valid_duration "$2"; then
				error "'$2' is not a valid duration"
				exit 1
			fi
			LOGS_DURATION=$2
			shift 2
			continue
		;;
		'-p'|'--loki-port')
			LOKI_PORT=$2
			shift 2
			continue
		;;
		'-c'|'--no-strip-color')
			NO_STRIP_COLOR=1
			shift
			continue
		;;
		'-j'|'--include-journal')
			INCLUDE_JOURNAL=1
			shift
			continue
		;;
		'-q'|'--quiet')
			QUIET=1
			shift
			continue
		;;
		'--')
			shift
			break
		;;
		*)
			error "Unknown option"
			usage
			exit 1
		;;
	esac
done

# Parse positional args
if [[ $# -gt 1 ]]; then
	error "Too many arguments"
	usage
	exit 1
fi
OUTPUT_DIR=${1%/} # Remove trailing slash
if [[ -n "$OUTPUT_DIR" && ! -d "$OUTPUT_DIR" ]]; then
	error "Directory does not exist: $OUTPUT_DIR"
	exit 1
fi

# Temporary dir where log files are collected
if ! TEMP_DIR=$(mktemp --directory); then
	error "Failed to create temporary directory"
	exit 1
fi

start_loki_port_forward

collect_loki_logs
collect_journalctl_logs
collect_var_log
collect_commands_output

if [[ -z "$NO_STRIP_COLOR" ]]; then
	strip_color_codes
fi

create_archive

 

Save and EXIT

:wq

 

Step 4. Give permissions to /tmp/generate-support-bundle.sh file

chmod +x /tmp/generate-support-bundle.sh

 

Step 5. Generate the support bundle

/tmp/generate-support-bundle.sh

Use one of the following parameters for specific requirements

Options:
		    -s, --since DURATION     collect logs for the last DURATION hours or minutes (default: ${DEFAULT_LOGS_DURATION});
		                             possible sufixes: "h" (hours), "m" (minutes); only affects Loki and systemd
		    -p, --loki-port PORT     forward PORT to port 3100 inside Loki pod (default: choose port automatically)
		    -c, --no-strip-color     do not strip color codes from collected files
		    -j, --include-journal    include binary logs from /var/log/journal in the bundle; this is usually not
		                             necessary as systemd logs are already collected as plaintext with \`journalctl\`
		    -q, --quiet              suppress non-error messages
		    -h, --help               display this help text and exit

 

Step 6 (optional). Upload support bundle ("How to Upload Files in SynetoOS 6")