NGINX Plus R33 requires NGINX Instance Manager 2.18 or later
If your NGINX data plane instances are running NGINX Plus R33 or later, you must upgrade to NGINX Instance Manager 2.18 or later to support usage reporting. NGINX Plus R33 instances must report usage data to the F5 licensing endpoint or NGINX Instance Manager. Otherwise, they will stop processing user traffic.

For more details about usage reporting and enforcement, see About solution licenses.

Report usage to F5 in a disconnected environment

Overview

In a disconnected environment without internet access, NGINX Plus sends usage data to NGINX Instance Manager. You’ll need to download the usage report from NGINX Instance Manager and submit it to F5 from a location with internet access. After F5 verifies the report, you can download the acknowledgement, which you must upload back to NGINX Instance Manager.

Before you begin

Before submitting usage data to F5, first configure NGINX Plus to report telemetry data to NGINX Instance Manager.

Configure NGINX Plus to report usage data to NGINX Instance Manager

To configure NGINX Plus (R33 and later) to report usage data to NGINX Instance Manger:

  1. Open port 443 for NGINX Instance Manager.

  2. On each NGINX Plus instance, update the usage_report directive in the mgmt block of the NGINX configuration (/etc/nginx/nginx.conf) to point to your NGINX Instance Manager host:

    mgmt {
      usage_report endpoint=<NGINX-INSTANCE-MANAGER-FQDN>;
    }
    
  3. Reload NGINX:

    nginx -s reload
    

Submit usage report

Using the REST API
You can use tools like curl or Postman to interact with the NGINX Instance Manager REST API. The API URL is https://<NIM-FQDN>/api/[nim|platform]/<API_VERSION>, and each request requires authentication. For more details on authentication options, see the API Overview.

Submit usage report with a bash script

To submit a usage report in a disconnected environment, use the provided license_usage_offline.sh script. Run this script on a system that can access NGINX Instance Manager and connect to https://product.apis.f5.com/ on port 443. Replace each placeholder with your specific values.


  1. Download license_usage_offline.sh.

  2. Run the following command to allow the script to run:

    chmod +x <path-to-script>/license_usage_offline.sh
    
  3. Run the script. Replace each placeholder with your specific values:

    ./license_usage_offline.sh \
      -j <license-filename>.jwt \
      -i <NIM-IP-address> \
      -u admin \
      -p <password> \
      -o report.zip \
      -s telemetry
    

    This command downloads the usage report (report.zip), submits the report to F5 for acknowledgment, and uploads the acknowledgment back to NGINX Instance Manager.

View the full contents of the license_usage_offline.sh script
#!/bin/bash

# Function to encode the username and password to base64
encode_base64() {
  echo -n "$1:$2" | base64
}

# Print help text and exit.
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
  echo
  echo "Usage: $0 -j <path-to-JWT> -i <NIM-IP> -u <username> -p <password> -o <output zip file> -s <step>"
  echo
  echo "This script allows you to license NGINX Instance Manager and submit usage reports to F5 in a disconnected environment."
  echo "It needs to be run on a system that can reach NGINX Instance Manager and the following endpoint on port 443: https://product.apis.f5.com/"
  echo
  echo "If NGINX Instance Manager is connected to the internet (Connected Mode), please use the UI."
  echo "This script is only necessary if NGINX Instance Manager is set to Disconnected Mode."
  echo
  echo "For Licensing in Disconnected Mode:"
  echo "$0 -j my-jwt.jwt -i <NIM-IP> -u admin -p <password> -o report.zip -s initial"
  echo 
  echo "For Usage Reporting in Disconnected Mode:"
  echo "$0 -j my-jwt.jwt -i <NIM-IP> -u admin -p <password> -o report.zip -s telemetry"
  echo 
  echo "Note: Since NGINX Instance Manager comes with self-signed certificates by default, the --insecure flag is set in this script to run specific Curl commands." 
  echo "You can alter this script if you wish to change from self-signed to verified certificates in Instance Manager."
  echo 
  exit 1
fi

if ! command -v jq &> /dev/null; then
	echo -e "\nPlease install jq (https://jqlang.github.io/jq/) as it is required to run the script\n"
	exit 1
fi

# Parse command-line arguments
while getopts ":j:i:u:p:o:s:" opt; do
  case $opt in
    j) jwt_file="$OPTARG" ;;
    i) ip_address="$OPTARG" ;;
    u) username="$OPTARG" ;;
    p) password="$OPTARG" ;;
    o) output_file="$OPTARG" ;;
    s) step="$OPTARG" ;;
    \?) echo "Invalid option -$OPTARG" >&2 ;;
  esac
done


# Check if JWT_FILE is not empty and if the file exists
if [ -z "$jwt_file" ]; then
    echo "JWT file path is not defined. Please set the JWT_FILE variable."
    exit 1
elif [ ! -f "$jwt_file" ]; then
    echo "JWT file not found: $jwt_file"
    exit 1
else
    echo "JWT file found: $jwt_file"
fi


# Read the Bearer token from the file
bearer_token=$(<"$jwt_file")

# Encode the username and password to base64 for Basic Authorization
basic_auth=$(encode_base64 "$username" "$password")


# Initialize report_save_path if it's empty
report_save_path=${output_file:-"report.zip"}

############################################################################
# Step 1: Upload JWT to NGINX Instance Manager and Download telemetry report
############################################################################
echo "###################################################################"
echo "# (if not already licensed) Upload JWT and Download the Telemetry report from NGINX Instance Manager  #"
echo "###################################################################"
if [ "$step" == "initial" ]; then

    # Function to print banner
    print_banner() {
        echo "===================================="
        echo "$1"
        echo "===================================="
    }

    # Step 1: POST request
    print_banner "Executing Step 1: POST request"
    post_response=$(curl -k --location "https://$ip_address/api/platform/v1/license?telemetry=true" \
        --header "Origin: https://$ip_address" \
        --header "Referer: https://$ip_address/ui/settings/license" \
        --header "Content-Type: application/json" \
        --header "Authorization: Basic $basic_auth" \
        --data "{
            \"metadata\": {
                \"name\": \"license\"
            },
            \"desiredState\": {
                \"content\": \"$bearer_token\"
            }
        }")
    # Use jq to extract modeOfOperation
    modeOfOperation=$(jq -r '.currentStatus.modeOfOperation' <<< "$post_response")

    # Output the modeOfOperation
    echo "Mode of Operation: $modeOfOperation"

    # Parse the modeOfOperation from the response
    echo $modeOfOperation
    # Step 2: Polling for the license status with conditional logic based on modeOfOperation
    while true; do
        print_banner "Checking License Status"

        response=$(curl -k "https://$ip_address/api/platform/v1/license" \
            -H "accept: application/json" \
            -H "authorization: Basic $basic_auth" \
            -H "referer: https://$ip_address/ui/settings/license" \
            --insecure)
        fileType=$(jq -r '.currentStatus.state.currentInstance.fileType' <<< "$response")
        status=$(jq -r '.currentStatus.state.currentInstance.status' <<< "$response")
        telemetry=$(jq -r '.currentStatus.state.currentInstance.telemetry' <<< "$response")

        echo $fileType
        echo $status
        echo $telemetry
        if [[ "$modeOfOperation" == "CONNECTED" ]]; then
        if [[ "$fileType" == "JWT" && "$status" == "ACTIVATED" && "$telemetry" == "true" ]]; then
                echo "Connected mode: Desired response received!"
                break
            else
                echo "Connected mode: Waiting for the desired response..."
            fi
        elif [[ "$modeOfOperation" == "DISCONNECTED" ]]; then
            if [[ "$fileType" == "JWT" && "$status" == "INITIALIZE_ACTIVATION_COMPLETE" ]]; then
                echo "Disconnected mode: Desired response received!"
                break
            else
                echo "Disconnected mode: Waiting for the desired response..."
            fi
        fi

        sleep 5  # Poll every 5 seconds
    done

    # Step 3: PUT request
    print_banner "Executing Step 2: PUT request"
    put_response=$(curl -k --location --request PUT "https://$ip_address/api/platform/v1/license?telemetry=true" \
        --header "Origin: https://$ip_address" \
        --header "Referer: https://$ip_address/ui/settings/license" \
        --header "Content-Type: application/json" \
        --header "Authorization: Basic $basic_auth" \
        --data '{
            "desiredState": {
                "content": "",
                "type": "JWT",
                "features": [
                    {"limit": 0, "name": "NGINX_NAP_DOS", "valueType": ""},
                    {"limit": 0, "name": "IM_INSTANCES", "valueType": ""},
                    {"limit": 0, "name": "TM_INSTANCES", "valueType": ""},
                    {"limit": 0, "name": "DATA_PER_HOUR_GB", "valueType": ""},
                    {"limit": 0, "name": "NGINX_INSTANCES", "valueType": ""},
                    {"limit": 0, "name": "NGINX_NAP", "valueType": ""},
                    {"limit": 0, "name": "SUCCESSFUL_API_CALLS_MILLIONS", "valueType": ""},
                    {"limit": 0, "name": "IC_PODS", "valueType": ""},
                    {"limit": 0, "name": "IC_K8S_NODES", "valueType": ""}
                ]
            },
            "metadata": {
                "name": "license"
            }
        }')

    echo "Response from Step 2: PUT request:"

    # Step 4: Polling for the desired license status
    while true; do
        print_banner "Checking License Validation Status"

        response=$(curl -s "https://$ip_address/api/platform/v1/license" \
            -H "accept: application/json" \
            -H "authorization: Basic $basic_auth" \
            -H "referer: https://$ip_address/ui/settings/license" \
            --insecure)
        fileType=$(jq -r '.currentStatus.state.currentInstance.fileType' <<< "$response")
        status=$(jq -r '.currentStatus.state.currentInstance.status' <<< "$response")
        telemetry=$(jq -r '.currentStatus.state.currentInstance.telemetry' <<< "$response")


        if [[ "$modeOfOperation" == "CONNECTED" ]]; then
            if [[ "$fileType" == "JWT" && "$status" == "VALID" ]]; then
                echo "Connected mode: Desired response received!"
                break
            else
                echo "Connected mode: Current license response does not meet conditions. Retrying..."
            fi
        elif [[ "$modeOfOperation" == "DISCONNECTED" ]]; then
            if [[ "$fileType" == "JWT" && "$status" == "CONFIG_REPORT_READY" ]]; then
                echo "Disconnected mode: Desired response received!"
                break
            else
                echo "Disconnected mode: Current license response does not meet conditions. Retrying..."
            fi
        fi

        sleep 5
    done

    # Step 5: Loop to check if licensed
    while true; do
        print_banner "Checking Licensed Status"

        licensed_response=$(curl -s "https://$ip_address/api/platform/v1/modules/licensed" \
            -H "accept: application/json" \
            -H "authorization: Basic $basic_auth" \
            -H "referer: https://$ip_address/ui/settings/license" \
            --insecure)

        licensed_status=$(jq -r '.licensed' <<< "$licensed_response" )
        if [[ "$modeOfOperation" == "CONNECTED" ]]; then

            if [[ "$licensed_status" == "true" ]]; then
                echo "The system is licensed on connected mode!"
                break
            else
                echo "The system is not licensed yet. Retrying..."
            fi
        elif [[ "$modeOfOperation" == "DISCONNECTED" ]]; then
            if [[ "$licensed_status" == "false" ]]; then
                echo "Upload JWT success on disconnected mode"
                break
        fi
        fi

        sleep 5
    done

    echo "Requests executed successfully."

#################################################
# Step 2: Download the telemetry report from NGINX Instance Manager #
#################################################
echo "#################################################"
echo "# Download the telemetry report from NGINX Instance Manager #"
echo "#################################################"

  echo "Executing for step: initial"
  download_usage_command="curl --insecure --location 'https://$ip_address/api/platform/v1/report/download?format=zip&reportType=initial' \
          --header 'accept: */*' \
          --header 'authorization: Basic $basic_auth' \
          --output \"$report_save_path\""

elif [ "$step" == "telemetry" ]; then
  echo "Executing for step: telemetry"
  prepare_usage_command="curl --insecure --location 'https://$ip_address/api/platform/v1/report/download?format=zip&reportType=telemetry&telemetryAction=prepare' \
          --header 'accept: application/json' \
          --header 'authorization: Basic $basic_auth' \
          --header 'referer: https://$ip_address/ui/settings/license'"

  download_usage_command="curl --insecure --location 'https://$ip_address/api/platform/v1/report/download?format=zip&reportType=telemetry&telemetryAction=download' \
  --header 'accept: */*' \
  --header 'authorization: Basic $basic_auth' \
  --output \"$report_save_path\""
fi
if [ "$step" == "telemetry" ]; then
  echo "Running telemetry stage: "

  # Run the saved command and store the response
  response=$(eval $prepare_usage_command)

  # Print the response
  echo "Response: $response"
  sleep 2
  # Validate if the response contains "Report generation in progress"
  if echo "$response" | grep -q '"telemetry":"Report generation in progress"'; then
    echo "Success: Report generation is in progress."
  else
    echo "Failure: Report generation not in progress or unexpected response."
    exit 1
  fi

    echo "Running command: $download_usage_command"
    eval $download_usage_command
  else
    echo "Running command: $download_usage_command"
    eval $download_usage_command
  fi

############################################################
# Step 3: Upload the telemetry report to F5's licensing endpoint
############################################################
echo "############################################################"
echo "# Upload the telemetry report to F5's licensing endpoint  #"
echo "############################################################"

echo "Uploading $report_save_path to telemetry endpoint..."
upload_command="curl --location 'https://product.apis.f5.com/ee/v1/entitlements/telemetry/bulk' \
--header 'Authorization: Bearer $bearer_token' \
--form 'file=@\"$report_save_path\"'"

response=$(eval $upload_command)
echo "Response from upload: $response"

############################################################
# Step 4: Extract the status ID from the response
############################################################
echo "############################################################"
echo "# Extract status ID from the response#"
echo "############################################################"

status_link=$(jq -r '.statusLink | split("/") | last' <<< "$response")
if [ -z "$status_link" ]; then
  echo "Failed to extract status link from response. Please try again."
  exit 1
fi
echo "Extracted status link ID: $status_link"

##############################################################
# Step 5: Check the status to ensure the acknowledgement file is ready to download
############################################################
echo "############################################################"
echo "# Check the status to ensure the acknowledgement file is ready to download     #"
echo "############################################################"

# Initialize variables for the loop
max_attempts=10
attempt=0

# Loop to check the status until percentageComplete and percentageSuccessful are 100 or the max attempts is reached
while [ $attempt -lt $max_attempts ]; do
  echo "Checking status for the upload... (Attempt: $((attempt + 1)))"

  # Run the curl command to get the status
  status_command="curl --location 'https://product.apis.f5.com/ee/v1/entitlements/telemetry/bulk/status/$status_link' --header 'Authorization: Bearer $bearer_token'"

  status_response=$(eval $status_command)

  # Extract values from the response
  percentage_complete=$(jq -r '.percentageComplete' <<< "$status_response")
  percentage_successful=$(jq -r '.percentageSuccessful' <<<  "$status_response")
  ready_for_download=$(jq -r '.readyForDownload' <<< "$status_response")

  echo "Percentage Complete: $percentage_complete"
  echo "Percentage Successful: $percentage_successful"
  echo "Ready for Download: $ready_for_download"

  # Check if the report is ready for download
  if [ "$percentage_complete" == "100" ] && [ "$percentage_successful" == "100" ] && [ "$ready_for_download" == "true" ]; then
    echo "File is ready for download."
    break
  else
    echo "File is not ready for download yet. Sleeping for 2 seconds..."
    sleep 2
  fi

  # Increment the attempt counter
  attempt=$((attempt + 1))
done

# If after 10 attempts it's still not ready, show a message
if [ $attempt -eq $max_attempts ]; then
  echo "Reached maximum attempts. The file is not ready for download."
  exit 1
fi

############################################################
# Step 6: Download the acknowledgement file from F5's licensing endpoint
############################################################
echo "############################################################"
echo "# Download the acknowledgement report from F5's licensing endpoint #"
echo "############################################################"

# Extract downloadLink
download_link=$(jq -r '.downloadLink | split("/") | last' <<< "$status_response")
echo $download_link
# Ensure the report_save_path is not empty
if [ -z "$report_save_path" ]; then
  # If the report_save_path is empty, set a default path and filename
  report_save_path="report_download.zip"
fi

echo "Downloading file from telemetry..."

download_command="curl --location 'https://product.apis.f5.com/ee/v1/entitlements/telemetry/bulk/download/$download_link' \
        --header 'Authorization: Bearer $bearer_token' \
        --output '$report_save_path'"

curl --location "https://product.apis.f5.com/ee/v1/entitlements/telemetry/bulk/download/$download_link" \
--header "Authorization: Bearer $bearer_token" \
--output "$report_save_path"

echo "Downloaded file saved as: $report_save_path"

############################################################
# Step 7: Upload the acknowledgement report to NGINX Instance Manager
############################################################
echo "############################################################"
echo "# Upload the acknowledgement report to NGINX Instance Manager   #"
echo "############################################################"

curl --insecure --location "https://$ip_address/api/platform/v1/report/upload" \
--header "Authorization: Basic $basic_auth" \
--form "file=@\"$report_save_path\"" \
--silent --output /dev/null

echo "Report acknowledgement successfully uploaded to NGINX Instance Manager $ip_address."

Submit usage report with curl

To submit a usage report using curl, complete each of the following steps in order.

Run these curl commands on a system that can access NGINX Instance Manager and connect to https://product.apis.f5.com/ on port 443. Replace each placeholder with your specific values.

Important:
The -k flag skips SSL certificate validation. Use this only if your NGINX Instance Manager is using a self-signed certificate or if the certificate is not trusted by your system.
  1. Prepare the usage report:

    curl -k --location 'https://<NIM-FQDN>/api/platform/v1/report/download?format=zip&reportType=telemetry&telemetryAction=prepare' \
    --header 'accept: application/json' \
    --header 'authorization: Basic <base64-encoded-credentials>' \
    --header 'referer: https://<NIM-FQDN>/ui/settings/license'
    
  2. Download the usage report from NGINX Instance Manager:

    curl -k --location 'https://<NIM-FQDN>/api/platform/v1/report/download?format=zip&reportType=telemetry&telemetryAction=download' \
    --header 'accept: */*' \
    --header 'authorization: Basic <base64-encoded-credentials>' \
    --output report.zip
    
  3. Submit the usage report to F5 for verification:

    curl --location 'https://product.apis.f5.com/ee/v1/entitlements/telemetry/bulk' \
    --header "Authorization: Bearer $(cat /path/to/jwt-file)" \
    --form 'file=@"<path-to-report>.zip"'
    

    After running this command, look for the “statusLink” in the response. The report-id is the last part of the “statusLink” value (the UUID). For example:

    {"statusLink":"/status/2214e480-3401-43a3-a54c-9dc501a01f83"}
    

    In this example, the report-id is 2214e480-3401-43a3-a54c-9dc501a01f83.

    You’ll need to use your specific report-id in the following steps.

  4. Check the status of the usage acknowledgement:

    Replace <report-id> with your specific ID from the previous response.

    curl --location 'https://product.apis.f5.com/ee/v1/entitlements/telemetry/bulk/status/<report-id>' \
    --header "Authorization: Bearer $(cat /path/to/jwt-file)"
    
  5. Download the usage acknowledgement from F5:

    curl --location 'https://product.apis.f5.com/ee/v1/entitlements/telemetry/bulk/download/<report-id>' \
    --header "Authorization: Bearer $(cat /path/to/jwt-file)" \
    --output <path-to-acknowledgement>.zip
    
  6. Upload the usage acknowledgement to NGINX Instance Manager:

    curl -k --location 'https://<NIM-FQDN>/api/platform/v1/report/upload' \
    --header 'Authorization: Basic <base64-encoded-credentials>' \
    --form 'file=@"<path-to-acknowledgement>.zip"'
    

Submit usage report with the web interface

Download usage report

Download the usage report to send to F5:

  • On the License > Overview page, select Download License Report.

Submit usage report to F5

You need to submit the usage report to F5 and download the acknowledgment over REST. To do do, follow steps 3–5 in the REST tab in this section.

Upload the usage acknowledgement to NGINX Instance Manager

To upload the the usage acknowledgement:

  1. On the License > Overview page, select Upload Usage Acknowledgement.
  2. Upload the acknowledgement by selecting Browse or dragging the file into the form.
  3. Select Add.

What’s reported

The following JSON example shows the NGINX Plus usage data sent to F5 in a POST request, every hour by default.

{
    "version": "<nginx_version>",
    "uuid": "<nginx_uuid>",
    "nap": "<active/inactive>", // status of NGINX App Protect
    "http": {
        "client": {
            "received": 0, // bytes received
            "sent": 0, // bytes sent
            "requests": 0 // number of HTTP requests processed
        },
        "upstream": {
            "received": 0, // bytes received
            "sent": 0 // bytes sent
        }
    },
    "stream": {
        "client": {
            "received": 0, // bytes received
            "sent": 0 // bytes sent
        },
        "upstream": {
            "received": 0, // bytes received
            "sent": 0 // bytes sent
        }
    },
    "workers": 0, // number of worker processes running
    "uptime": 0, // number of seconds the instance has been running
    "reloads": 0, // number of times the instance has been reloaded
    "start_time": "epoch", // start time of data collection for the report
    "end_time": "epoch" // end time of data collection for the report
}