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.

Add a license in a disconnected environment

Overview

This guide shows you how to add a license to NGINX Instance Manager in a disconnected (offline) environment. In this setup, systems don’t have internet access. You’ll download and apply your subscription’s JSON Web Token (JWT) license, then verify your entitlements with F5.

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.

Before you begin

Set the operation mode to disconnected

To configure NGINX Instance Manager for a network-restricted environment, you need to set the mode_of_operation to disconnected in the configuration file.

  1. Open the /etc/nms/nms.conf file and add the following in the integrations:license section:

    integrations:
        license:
            mode_of_operation: disconnected
    
  2. Restart NGINX Instance Manager:

    sudo systemctl restart nms
    

Download the JWT license from MyF5

  1. Log in to MyF5.
  2. Go to My Products & Plans > Subscriptions to see your active subscriptions.
  3. Find your NGINX products or services subscription, and select the Subscription ID for details.
  4. Download the JSON Web Token from the subscription page.

Add license and submit initial usage report

Add license and submit initial usage report with a bash script

To add a license and submit the initial 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.

Important: The script to add a license won’t work if a license has already been added.


  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 initial
    

    This command adds the license, downloads the initial 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."

Add license and submit initial usage report with curl

To license NGINX Instance Manager, complete each of the following steps in order.

Important: The curl command to add a license won’t work if a license has already been added.

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. Add the license to NGINX Instance Manager:

    curl -k --location 'https://<NIM-FQDN>/api/platform/v1/license?telemetry=true' \
    --header 'Origin: https://<NIM-FQDN>' \
    --header 'Referer: https://<NIM-FQDN>/ui/settings/license' \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Basic <base64-encoded-credentials>' \
    --data '{
      "metadata": {
        "name": "license"
      },
      "desiredState": {
        "content": "<JSON-web-token>"
      }
    }'
    
  2. Poll the license status on NGINX Instance Manager:

    Use this command to check the current license status. Look for INITIALIZE_ACTIVATION_COMPLETE or CONFIG_REPORT_READY in the status field. Poll periodically if necessary.

    curl -k "https://<NIM-FQDN>/api/platform/v1/license" \
    --header "accept: application/json" \
    --header "authorization: Basic <base64-encoded-credentials>" \
    --header "referer: https://<NIM-FQDN>/ui/settings/license"
    
  3. Update the license configuration on NGINX Instance Manager:

    This step ensures that the license configuration is fully applied.

    curl -k --location --request PUT "https://<NIM-FQDN>/api/platform/v1/license?telemetry=true" \
    --header "Origin: https://<NIM-FQDN>" \
    --header "Referer: https://<NIM-FQDN>/ui/settings/license" \
    --header "Content-Type: application/json" \
    --header "Authorization: Basic <base64-encoded-credentials>" \
    --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"
      }
    }'
    
  4. Download the initial usage report:

    curl -k --location 'https://<NIM-FQDN>/api/platform/v1/report/download?format=zip&reportType=initial' \
    --header 'accept: */*' \
    --header 'Authorization: Basic <base64-encoded-credentials>' \
    --output report.zip
    
  5. 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.

  6. Check the status of the usage acknowledgment:

    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)"
    
  7. 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
    
  8. 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"'
    

Add license and submit initial usage report with the web interface

Add license

To add a license:

  1. Go to the FQDN of your NGINX Instance Manager host and log in.
  2. Select the Settings (gear) icon. The Licenses > Overview opens at https://<NIM_FQDN>/ui/settings/license.
  3. Select Get Started.
  4. Upload your license file by selecting Browse or dragging the file into the form.
  5. Select Add.

Download initial usage report

Download the initial 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 5–7 in the REST tab in this section.

Upload the usage acknowledge 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.