Modern IT environments rely heavily on scalable and high-performance storage solutions. One such technology is iSCSI (Internet Small Computer System Interface), which allows block-level storage data to be transmitted over IP networks. Managing and monitoring iSCSI connections is essential for system administrators, especially in complex enterprise or MSP-managed infrastructures.
This blog post introduces a shell script that empowers Linux admins to get iSCSI connection details in Linux with shell scripting—ideal for diagnostics, reporting, and integration with RMM platforms like NinjaOne.
Background
For IT professionals, visibility into the underlying storage fabric is crucial. iSCSI configurations, often invisible to higher-level applications, are foundational in virtualization clusters, high-availability environments, and data centers. Yet, Linux provides only command-line tools like iscsiadm for querying such information, and they typically return unstructured data. This can hinder automation and real-time reporting—key elements for modern IT operations.
This shell script bridges that gap. Not only does it collect detailed session and connection information, but it also formats the output into HTML tables and optionally pushes it into a NinjaOne WYSIWYG custom field. This enables seamless visibility within RMM dashboards—no manual parsing or log-hunting required.
The Script
#!/usr/bin/env bash
# Description: Gets the details of all iSCSI devices on the system.
# By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://www.ninjaone.com/terms-of-use.
# Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms.
# Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party.
# Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider.
# Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations.
# Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks.
# Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script.
# EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA).
#
# Release Notes: Initial Release
#
# Usage: [-WYSIWYGCustomFieldName <Arg>]
#
# Preset Parameter: --help
# Displays the help menu.
#
# Preset Parameter: --WYSIWYGCustomFieldName "ReplaceMeWithAnyMultilineCustomField"
# Optionally specify the name of a WYSIWYG custom field to save the results to.
# Functions
# Print an error message and exit with a specific status code
die() {
local _ret="${2:-1}"
test "${_PRINT_HELP:-no}" = yes && print_help >&2
echo "$1" >&2
exit "${_ret}"
}
print_help() {
printf '\n\n%s\n\n' 'Usage: [-WYSIWYGCustomFieldName <Arg>]'
printf '%s\n' 'Preset Parameter: --help'
printf '\t%s\n' "Displays the help menu."
printf '%s\n' 'Preset Parameter: --WYSIWYGCustomFieldName "ReplaceMeWithAnyMultilineCustomField"'
printf '\t%s\n' "Optionally specify the name of a WYSIWYG custom field to save the results to."
}
convertToHTMLTable() {
local _arg_delimiter=" "
local _arg_inputObject
# Process command-line arguments for the function.
while test $# -gt 0; do
_key="$1"
case "$_key" in
--delimiter | -d)
test $# -lt 2 && echo "[Error] Missing value for the required argument" >&2 && return 1
_arg_delimiter=$2
shift
;;
--*)
echo "[Error] Got an unexpected argument" >&2
return 1
;;
*)
_arg_inputObject=$1
;;
esac
shift
done
# Handles missing input by checking stdin or returning an error.
if [[ -z $_arg_inputObject ]]; then
if [ -p /dev/stdin ]; then
_arg_inputObject=$(cat)
else
echo "[Error] Missing input object to convert to table" >&2
return 1
fi
fi
local htmlTable="<table>\n"
htmlTable+=$(printf '%b' "$_arg_inputObject" | head -n1 | awk -F "$_arg_delimiter" '{
printf "<tr>"
for (i=1; i<=NF; i+=1)
{ printf "<th>"$i"</th>" }
printf "</tr>"
}')
htmlTable+="\n"
htmlTable+=$(printf '%b' "$_arg_inputObject" | tail -n +2 | awk -F "$_arg_delimiter" '{
printf "<tr>"
for (i=1; i<=NF; i+=1)
{ printf "<td>"$i"</td>" }
print "</tr>"
}')
htmlTable+="\n</table>"
printf '%b' "$htmlTable" '\n'
}
# Function to set a custom field
function SetCustomField() {
if [[ -z "$1" ]] || [[ -z "$2" ]]; then
echo "[Error] Missing required arguments."
return 1
fi
local customfieldName=$1
local customfieldValue=$2
if [ -f "${NINJA_DATA_PATH}/ninjarmm-cli" ]; then
if [ -x "${NINJA_DATA_PATH}/ninjarmm-cli" ]; then
if "$NINJA_DATA_PATH"/ninjarmm-cli get "$customfieldName" >/dev/null; then
# check if the value is greater than 10000 characters
if [ ${#customfieldValue} -gt 10000 ]; then
echo "[Warn] Custom field value is greater than 10000 characters"
fi
if ! echo "${customfieldValue::10000}" | "$NINJA_DATA_PATH"/ninjarmm-cli set --stdin "$customfieldName"; then
echo "[Warn] Failed to set custom field"
else
echo "[Info] Custom field value set successfully"
fi
else
echo "[Warn] Custom Field ($customfieldName) does not exist or agent does not have permission to access it"
fi
else
echo "[Warn] ninjarmm-cli is not executable"
fi
else
echo "[Warn] ninjarmm-cli does not exist"
fi
}
# Parse the command-line arguments
parse_commandline() {
while test $# -gt 0; do
_key="$1"
case "$_key" in
-w | --WYSIWYGCustomFieldName)
test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1
_arg_WYSIWYGCustomFieldName="$2"
shift
;;
--WYSIWYGCustomFieldName=*)
_arg_WYSIWYGCustomFieldName="${_key##--wysiwygCustomFieldName=}"
;;
-w*)
_arg_WYSIWYGCustomFieldName="${_key##-w}"
;;
--help)
_PRINT_HELP=yes die "" 0
;;
-h)
_PRINT_HELP=yes die "" 0
;;
*)
_PRINT_HELP=yes die "[Error] Got an unexpected argument '$1'" 1
;;
esac
shift
done
}
parse_commandline "$@"
# If script form variables are used, replace the command line parameters with their value.
if [[ -n "${wysiwygCustomFieldName}" ]]; then
_arg_WYSIWYGCustomFieldName="$wysiwygCustomFieldName"
fi
# If no valid custom field was provided, display an error message and exit the script.
# if [[ -z "${_arg_WYSIWYGCustomFieldName}" ]]; then
# _PRINT_HELP=yes die "[Error] You must provide a valid WYSIWYG custom field name." 1
# fi
# Check if the root user is running the script
if [[ $EUID -ne 0 ]]; then
die "[Error] This script must be run as root." 1
fi
if ! [ "$(command -v iscsiadm)" ]; then
die "[Error] The command iscsiadm not installed." 1
fi
if ! [ -f "/etc/iscsi/iscsid.conf" ]; then
die "[Error] /etc/iscsi/iscsid.conf is not present." 1
fi
# Print Initiator Name
_connection_identifier=$(grep -v "#" /etc/iscsi/initiatorname.iscsi 2>/dev/null | awk -F= '{print $2}')
# Get the iSCSI session details
session=$(iscsiadm -m session -P 3 2>&1)
# HTML title variables
_title_connections="iSCSI Connections"
_title_sessions="iSCSI Sessions"
# Delimiter for result parsing
_delimiter=";"
# iSCSI Connections
declare -g _iscsi_connections=""
declare -g _initiator_address=""
declare -g _initiator_port=""
declare -g _target_address=""
declare -g _target_port=""
# iSCSI Sessions
declare -g _iscsi_sessions=""
declare -g _authentication_type=""
declare -g _initiator_name=""
declare -g _initiator_node_address=""
declare -g _initiator_node_portal_address=""
declare -g _initiator_side_identifier=""
declare -g _is_connected=""
declare -g _is_data_digest=""
declare -g _is_persistent=""
declare -g _number_of_luns=0
declare -g _session_identifier=""
declare -g _target_node_address=""
declare -g _target_side_identifier=""
while read -r line; do
if [[ "$line" == "Target:"* ]]; then
if [[ -n "${_initiator_address}" ]]; then
# Add results
_iscsi_connections="${_iscsi_connections}\n"
_iscsi_connections="${_iscsi_connections}${_connection_identifier}${_delimiter}"
_iscsi_connections="${_iscsi_connections}${_initiator_address}${_delimiter}"
_iscsi_connections="${_iscsi_connections}${_initiator_port}${_delimiter}"
_iscsi_connections="${_iscsi_connections}${_target_address}${_delimiter}"
_iscsi_connections="${_iscsi_connections}${_target_port}"
_iscsi_sessions="${_iscsi_sessions}\n"
_iscsi_sessions="${_iscsi_sessions}${_authentication_type}${_delimiter}"
_iscsi_sessions="${_iscsi_sessions}${_initiator_name}${_delimiter}"
_iscsi_sessions="${_iscsi_sessions}${_initiator_node_address}${_delimiter}"
_iscsi_sessions="${_iscsi_sessions}${_initiator_node_portal_address}${_delimiter}"
_iscsi_sessions="${_iscsi_sessions}${_initiator_side_identifier}${_delimiter}"
_iscsi_sessions="${_iscsi_sessions}${_is_connected}${_delimiter}"
_iscsi_sessions="${_iscsi_sessions}${_is_data_digest}${_delimiter}"
_iscsi_sessions="${_iscsi_sessions}${_is_persistent}${_delimiter}"
_iscsi_sessions="${_iscsi_sessions}${_number_of_luns}${_delimiter}"
_iscsi_sessions="${_iscsi_sessions}${_session_identifier}${_delimiter}"
_iscsi_sessions="${_iscsi_sessions}${_target_node_address}${_delimiter}"
_iscsi_sessions="${_iscsi_sessions}${_target_side_identifier}"
fi
# Reset the variables
# iSCSI Connections
_initiator_address=""
_initiator_port=""
_target_address=""
_target_port=""
# iSCSI Sessions
_authentication_type=""
_initiator_name=""
_initiator_node_address=""
_initiator_node_portal_address=""
_initiator_side_identifier=""
_is_connected=""
_is_data_digest=""
_is_persistent=""
_number_of_luns=0
_session_identifier=""
_target_node_address=""
_target_side_identifier=""
# Get the target
connection_identifier=$(echo "$line" | cut -d ':' -f 2-3 | awk '{print $1}')
_target_node_address=$(echo "$connection_identifier" | cut -d ':' -f 1-2 | awk '{$1=$1;print}')
_target_address=$(echo "$connection_identifier" | cut -d ':' -f 1 | awk '{$1=$1;print}')
_target_side_identifier=$(echo "$connection_identifier" | cut -d ':' -f 2 | awk '{print $1}')
fi
if [[ "$line" == *"Current Portal:"* ]]; then
portal=$(echo "$line" | cut -d ':' -f 2-3)
_initiator_node_portal_address=$(echo "$portal" | cut -d ':' -f 1-2 | cut -d ',' -f 1 | awk '{$1=$1;print}')
_initiator_port=$(echo "$portal" | cut -d ':' -f 2 | cut -d ',' -f 1 | awk '{$1=$1;print}')
_target_port=$(echo "$portal" | cut -d ':' -f 2 | cut -d ',' -f 1 | awk '{$1=$1;print}')
fi
if [[ "$line" == *"Persistent Portal:"* ]]; then
_is_persistent="Yes"
fi
if [[ "$line" == *"Iface Name:"* ]]; then
_initiator_name=$(echo "$line" | cut -d ':' -f 2 | awk '{$1=$1;print}')
fi
if [[ "$line" == *"Iface Initiatorname:"* ]]; then
initiator=$(echo "$line" | cut -d ':' -f 2-4)
_initiator_node_address=$(echo "$initiator" | cut -d ':' -f 1-2 | awk '{$1=$1;print}')
_initiator_side_identifier=$(echo "$initiator" | cut -d ':' -f 3 | awk '{$1=$1;print}')
_initiator_address=$(echo "$initiator" | cut -d ':' -f 1 | awk '{$1=$1;print}')
fi
if [[ "$line" == *"iSCSI Connection State:"* ]]; then
_is_connected=$(echo "$line" | cut -d ':' -f 2 | awk '{$1=$1;print}')
fi
if [[ "$line" == *"SID:"* ]]; then
_session_identifier=$(echo "$line" | cut -d ':' -f 2 | awk '{$1=$1;print}')
fi
if [[ "$line" == *"DataDigest:"* ]]; then
_is_data_digest=$(echo "$line" | cut -d ':' -f 2 | awk '{$1=$1;print}')
fi
if [[ "$line" == *"CHAP:"* ]]; then
_authentication_type="CHAP"
fi
if [[ "$line" == *"username: <empty>"* ]]; then
# If the username is empty, set auth_method to None
_authentication_type="None"
fi
if [[ "$line" == *"Lun:"* ]]; then
_number_of_luns=$((_number_of_luns + 1))
fi
done <<<"$session"
_iscsi_connections="${_iscsi_connections}\n${_connection_identifier}${_delimiter}${_initiator_address}${_delimiter}${_initiator_port}${_delimiter}${_target_address}${_delimiter}${_target_port}"
_iscsi_sessions="${_iscsi_sessions}\n${_authentication_type}${_delimiter}${_initiator_name}${_delimiter}${_initiator_node_address}${_delimiter}${_initiator_node_portal_address}${_delimiter}${_initiator_side_identifier}${_delimiter}${_is_connected}${_delimiter}${_is_data_digest}${_delimiter}${_is_persistent}${_delimiter}${_number_of_luns}${_delimiter}${_session_identifier}${_delimiter}${_target_node_address}${_delimiter}${_target_side_identifier}"
# Output results to Activity Feed
_console_iscsi_connections="Connection Identifier${_delimiter}Initiator Address${_delimiter}Initiator Port${_delimiter}Target Address${_delimiter}Target Port\n${_iscsi_connections}"
_console_iscsi_sessions="Auth Type${_delimiter}Name${_delimiter}Init Address${_delimiter}Init Portal Address${_delimiter}Init Side ID${_delimiter}Connected${_delimiter}Data Digest${_delimiter}Persistent${_delimiter}LUNs${_delimiter}SID${_delimiter}Tgt Node Address${_delimiter}Tgt Side Identifier\n${_iscsi_sessions}"
# Print the result to activity feed
echo -e "\n---${_title_connections}---"
echo -e "${_console_iscsi_connections}" | column -t -s ';'
echo -e "\n---${_title_sessions}---"
# Check if there are no active iSCSI sessions
if [[ "${session}" == *"No active sessions"* || $(echo "$session" | wc -l) -le 1 ]]; then
echo "No active iSCSI sessions"
# If no sessions are active, notify that no data to be saved to custom field
if [[ -n "${_arg_WYSIWYGCustomFieldName}" ]]; then
echo "[Info] No data to save to custom field."
fi
exit 0
else
echo -e "${_console_iscsi_sessions}" | column -t -s ';'
fi
# Add Headers
_iscsi_connections="Connection Identifier${_delimiter}Initiator Address${_delimiter}Initiator Port${_delimiter}Target Address${_delimiter}Target Port\n${_iscsi_connections}"
_iscsi_sessions="Authentication Type${_delimiter}Initiator Name${_delimiter}Initiator Node Address${_delimiter}Initiator Node Portal Address${_delimiter}Initiator Side Identifier${_delimiter}Is Connected${_delimiter}Is Data Digest${_delimiter}Is Persistent${_delimiter}Number of LUNs${_delimiter}Session Identifier${_delimiter}Target Node Address${_delimiter}Target Side Identifier\n${_iscsi_sessions}"
# Convert the CSV result to an HTML table
html_connections_result=$(convertToHTMLTable "$_iscsi_connections" --delimiter "${_delimiter}")
html_sessions_result=$(convertToHTMLTable "$_iscsi_sessions" --delimiter "${_delimiter}")
# Combine the results
html_result+="<h3>${_title_connections}</h3>"
html_result+="$html_connections_result"
html_result+="<h3>${_title_sessions}</h3>"
html_result+="$html_sessions_result"
if [[ -n "${_arg_WYSIWYGCustomFieldName}" ]]; then
SetCustomField "$_arg_WYSIWYGCustomFieldName" "$html_result"
fi
Detailed Breakdown
Here’s how the script functions, step by step:
1. Pre-checks and Argument Parsing
- Root Privileges: The script must be run as root to query iSCSI sessions.
- Dependency Checks: Verifies that iscsiadm is installed and that /etc/iscsi/iscsid.conf exists.
- Optional Argument: Accepts a –WYSIWYGCustomFieldName parameter to specify a NinjaOne custom field for output.
2. Command Execution and Data Collection
- Runs iscsiadm -m session -P 3 to extract verbose session information.
- Parses data such as:
- Connection state
- Authentication type (CHAP or None)
- Target and initiator addresses
- Portal and session IDs
- Number of LUNs (Logical Unit Numbers)
3. Data Structuring
- Stores connection details into semicolon-delimited strings.
- Formats both “Connections” and “Sessions” into readable table-like outputs.
4. HTML Conversion
A convertToHTMLTable function transforms the delimited data into valid HTML tables. This allows for rich display within HTML-supported UIs like the NinjaOne WYSIWYG field.
5. Optional Output to NinjaOne
If a valid WYSIWYG field is provided, the script uses ninjarmm-cli to push the HTML-formatted result into the custom field—ensuring the data is easily accessible from the NinjaOne dashboard.
6. Visual Console Output
In addition to pushing data into NinjaOne, the script prints both connection and session summaries directly to the console using column -t for aligned formatting.
Potential Use Cases
Case Study: MSP Monitoring High-Availability iSCSI Storage
A managed service provider maintains a cluster of Linux servers connected to a centralized SAN via iSCSI. Occasionally, one or more servers experience storage latency. The MSP deploys this script via NinjaOne to automatically run weekly, storing the session state, target IPs, and LUN counts into custom fields. When latency is detected, the team can quickly correlate storage changes with performance regressions—saving hours of diagnostic time.
Comparisons
| Method | Description | Automation Ready | Visualization | NinjaOne Integration |
| iscsiadm manually | Raw CLI tool | ❌ | ❌ | ❌ |
| Custom Bash Parsing | Tailored parsing logic | ⚠️ | ❌ | ❌ |
| This Script | Full automation + HTML output | ✅ | ✅ | ✅ |
Other methods require manual interpretation or external tools. This script offers an end-to-end, automated solution tailored for RMMs.
Implications
By using shell scripting in Linux to get iSCSI connection details, IT teams gain structured visibility into storage layers—often the root of complex performance or reliability issues. Moreover, integrating this visibility directly into RMM dashboards empowers faster triage, reduces downtime, and enhances transparency for clients.
Security-wise, exposing session-level information also helps in auditing. Identifying which targets are connected and whether CHAP authentication is in place is vital for ensuring secure configurations.
Recommendations
- Schedule Periodic Runs: Use NinjaOne policies or automation to run this script at regular intervals.
- Monitor Field Size: Ensure custom fields aren’t truncated due to character limits.
- Secure Execution: Always run with root privileges on secure, trusted agents.
- Combine with Alerts: Create NinjaOne alerts based on field values (e.g., “No active sessions” or “Unexpected target”).
Final Thoughts
NinjaOne users looking to deepen their monitoring and diagnostics capabilities should strongly consider deploying this script. It exemplifies how shell scripting in Linux can be leveraged to get iSCSI connection details in a way that is both automation-ready and human-readable. With built-in integration into NinjaOne’s custom fields, it turns obscure storage session data into actionable intelligence—enhancing operational efficiency and responsiveness.
For more advanced workflows, this script can serve as a template for extending into other protocol diagnostics, making NinjaOne not just a monitoring tool but a true operational command center.