How to Get iSCSI Connection Details in Linux with Shell Scripting

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.

FAQs

Q: Does this script modify any system configuration?

A: No. It is strictly read-only and safe for diagnostics.

Q: What happens if no iSCSI sessions are active?

A: It gracefully exits and logs that there are no active sessions, avoiding unnecessary field updates.

Q: Is there a character limit when writing to custom fields?

A: Yes. The script warns if the HTML output exceeds 10,000 characters, which is a typical limit for NinjaOne fields.

Q: What platforms is this compatible with?

A: This script is designed for Linux systems with iscsiadm installed and NinjaOne agents configured.

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.

Next Steps

Building an efficient and effective IT team requires a centralized solution that acts as your core service delivery tool. NinjaOne enables IT teams to monitor, manage, secure, and support all their devices, wherever they are, without the need for complex on-premises infrastructure.

Learn more about NinjaOne Remote Script Deployment, check out a live tour, or start your free trial of the NinjaOne platform.

Categories:

You might also like

×

See NinjaOne in action!

By submitting this form, I accept NinjaOne's privacy policy.

NinjaOne Terms & Conditions

By clicking the “I Accept” button below, you indicate your acceptance of the following legal terms as well as our 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 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).