From fafc8fac20ab65693838bb56cad7be5737e87c1a Mon Sep 17 00:00:00 2001 From: app Date: Mon, 1 Sep 2025 23:39:26 +0800 Subject: [PATCH] Add sf-org-lic: Salesforce license utilization reporting tool - New sf-org-lic script for comprehensive license reporting - Generates reports for User Licenses and Permission Set Licenses - Enhanced error handling with org validation and clear error messages - Follows consistent sf-org-* naming convention with -to/-hp options - Professional formatted output with totals and color coding - Removed problematic FeatureLicense queries (not available in most orgs) - Updated README.md with full documentation and installation instructions Fixes issues with original sf-licenses-report.sh: - Silent failures when org doesn't exist - Poor error handling and cryptic messages - SOQL query issues with FIELDS(ALL) - Inconsistent naming convention --- README.md | 44 +++++++++++- sf-org-lic | 191 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+), 1 deletion(-) create mode 100755 sf-org-lic diff --git a/README.md b/README.md index f87ae25..cbe7f12 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Authentication: Org and metadata: - **[`sf-org-create` / `sf-org-create.ps1`](#sf-org-create)** - Smart scratch org creation +- **[`sf-org-lic`](#sf-org-lic)** - Salesforce org license utilization report - **[`sf-org-info` / `sf-org-info.ps1`](#sf-org-info--sf-org-infops1)** - Quick org info, limits, and context - **[`sf-retrieve` / `sf-retrieve.ps1`](#sf-retrieve--sf-retrieveps1)** - Streamlined metadata retrieval (types, manifest, packages) @@ -83,7 +84,7 @@ sf-data-export -qy "SELECT Id FROM Account" -to myorg -fm csv ```bash chmod +x \ sf-deploy sf-dry-run sf-web-open sf-web-login sf-web-logout sf-check \ - sf-org-create sf-org-info sf-retrieve sf-test-run sf-apex-run \ + sf-org-create sf-org-lic sf-org-info sf-retrieve sf-test-run sf-apex-run \ sf-data-export sf-data-import sf-logs-tail ``` 3. Add the directory to your PATH or create symlinks in a directory that's already in your PATH: @@ -99,6 +100,7 @@ ln -s /path/to/sf-cli-wrapper/sf-web-login /usr/local/bin/sf-web-login ln -s /path/to/sf-cli-wrapper/sf-web-logout /usr/local/bin/sf-web-logout ln -s /path/to/sf-cli-wrapper/sf-check /usr/local/bin/sf-check ln -s /path/to/sf-cli-wrapper/sf-org-create /usr/local/bin/sf-org-create +ln -s /path/to/sf-cli-wrapper/sf-org-lic /usr/local/bin/sf-org-lic ln -s /path/to/sf-cli-wrapper/sf-org-info /usr/local/bin/sf-org-info ln -s /path/to/sf-cli-wrapper/sf-retrieve /usr/local/bin/sf-retrieve ln -s /path/to/sf-cli-wrapper/sf-test-run /usr/local/bin/sf-test-run @@ -174,6 +176,46 @@ sf-org-create -al TestOrg -dd 5 -nn --- +### [🏠](#salesforce-cli-wrapper-scripts) sf-org-lic + +Generate comprehensive Salesforce license utilization reports. + +**Usage:** +```bash +sf-org-lic -to ORG [-hp] +``` + +**Options:** +- `-to` - Target org alias or username (required) +- `-hp` - Show help message + +**Examples:** +```bash +# Generate license report for production org +sf-org-lic -to PROD-ORG + +# Generate report for specific user +sf-org-lic -to admin@company.com + +# Show help +sf-org-lic -hp +``` + +**Features:** +- **User Licenses** - Core Salesforce user license utilization +- **Permission Set Licenses** - Add-on feature license usage +- **Comprehensive totals** - Overall usage summary with remaining capacity +- **Clean formatting** - Professional tabular output +- **Error handling** - Clear messages for invalid orgs with suggestions + +**Output includes:** +- License name, total allocated, used count, and remaining capacity +- Color-coded totals summary +- Professional formatting suitable for reports + +--- + + ### [🏠](#salesforce-cli-wrapper-scripts) sf-org-info / sf-org-info.ps1 Display org information, limits, and list authenticated orgs. diff --git a/sf-org-lic b/sf-org-lic new file mode 100755 index 0000000..0b7a7fc --- /dev/null +++ b/sf-org-lic @@ -0,0 +1,191 @@ +#!/usr/bin/env bash +# Usage: ./sf-org-lic -to NUSHUB-PROD +# +# Salesforce org license utilization report with proper error handling: +# - Validates org exists before running queries +# - Reports clear error messages instead of silent failures +# - Uses strict error checking to catch more issues +# - Fixed SOQL queries to only use existing fields +# - Focuses on User Licenses and Permission Set Licenses (the main license types) +set -euo pipefail + +ORG="" +while [[ $# -gt 0 ]]; do + case "$1" in + -to|--target-org) ORG="$2"; shift 2;; + -hp|--help) + echo "Usage: $0 -to " + echo "" + echo "Generate Salesforce license utilization report for an org" + echo "" + echo "Options:" + echo " -to Target org alias or username (required)" + echo " -hp Show this help message" + echo "" + echo "Examples:" + echo " $0 -to PROD-ORG" + echo " $0 -to dev@company.com" + echo "" + echo "Reports:" + echo " • User Licenses - Core Salesforce user licenses" + echo " • Permission Set Licenses - Add-on feature licenses" + exit 0;; + *) echo "Unknown arg: $1" >&2; exit 1;; + esac +done +[[ -z "$ORG" ]] && { echo "Usage: $0 -to " >&2; exit 1; } + +command -v sf >/dev/null 2>&1 || { echo "'sf' CLI is required." >&2; exit 1; } +command -v jq >/dev/null 2>&1 || { echo "'jq' is required." >&2; exit 1; } +command -v column >/dev/null 2>&1 || { echo "'column' is required." >&2; exit 1; } + +BOLD="$(printf '\033[1m')"; DIM="$(printf '\033[2m')" +CYAN="$(printf '\033[36m')"; GREEN="$(printf '\033[32m')" +RESET="$(printf '\033[0m')" + +underline() { printf '%*s\n' "${#1}" '' | tr ' ' '-'; } + +# Helper function for error messages +error_exit() { echo "Error: $1" >&2; exit "${2:-1}"; } + +# Validate that the org exists and is authorized +validate_org() { + local org="$1" + echo "Validating org: $org..." >&2 + + if ! sf org display --target-org "$org" --json >/dev/null 2>&1; then + # Try to get available orgs to suggest alternatives + local available_orgs + available_orgs=$(sf org list --json 2>/dev/null | jq -r '.result.other[]?.alias // empty' 2>/dev/null | tr '\n' ', ' | sed 's/,$//' || echo "none") + if [[ -z "$available_orgs" ]]; then + available_orgs="none" + fi + error_exit "org alias '$org' not found or not authorized. Available orgs: $available_orgs" 2 + fi +} + +# Normalize records location across sf CLI variants +JQ_RECS_DEF='def recs: (.result.records? // .records? // .Result.records? // []);' + +run_json_with_error_handling() { + local soql="$1" + local json + local exit_code + + # Temporarily disable strict error checking for this function + set +e + json=$(sf data query --target-org "$ORG" --json --query "$soql" 2>&1) + exit_code=$? + set -e + + # If the sf command itself failed, return the special error code + if [[ $exit_code -ne 0 ]]; then + return 42 + fi + + # Check if the result contains error information (sf CLI error responses) + if echo "$json" | jq -e '.name // .error // (.message and (.commandName // .status))' >/dev/null 2>&1; then + local error_msg + error_msg=$(echo "$json" | jq -r '.message // .error // "Unknown error"' 2>/dev/null || echo "JSON parse error") + + # Return special error code for object not found/no access (non-fatal) + if [[ "$error_msg" == *"not supported"* ]] || [[ "$error_msg" == *"does not exist"* ]] || [[ "$error_msg" == *"INVALID_TYPE"* ]] || [[ "$error_msg" == *"No such column"* ]]; then + return 42 # Special return code for missing objects + fi + + error_exit "SOQL query failed: $error_msg" 4 + fi + + echo "$json" + return 0 +} + +print_table_or_empty() { + # $1=json, $2=jq expr (builds a TSV row array from each record), $3=headers CSV + local json="$1" jexpr="$2" headers="$3" + local count + count="$(echo "$json" | jq -r "$JQ_RECS_DEF recs | length" 2>/dev/null || echo 0)" + echo "$headers" | awk '{gsub(/,/, "\t"); print}' | column -t -s $'\t' + if [[ "$count" -eq 0 ]]; then + echo "(no rows)" + echo + return + fi + echo "$json" \ + | jq -r "$JQ_RECS_DEF recs[] | $jexpr | @tsv" \ + | column -t -s $'\t' + echo +} + +print_totals() { + local json="$1" + local total used remaining + total=$(echo "$json" | jq -r "$JQ_RECS_DEF [recs[] | (try .TotalLicenses|tonumber // 0)] | add // 0") + used=$( echo "$json" | jq -r "$JQ_RECS_DEF [recs[] | (try .UsedLicenses|tonumber // 0)] | add // 0") + remaining=$(( total - used )) + echo -e "${CYAN}Totals:${RESET} Total=${BOLD}${total}${RESET} Used=${BOLD}${used}${RESET} Remaining=${GREEN}${BOLD}${remaining}${RESET}" + echo +} + +header() { + echo + echo -e "${BOLD}Salesforce License Utilization — ${ORG}${RESET}" + echo -e "${DIM}Generated: $(TZ=Asia/Manila date '+%Y-%m-%d %H:%M:%S %Z')${RESET}" + echo +} +section() { echo -e "${BOLD}$1${RESET}"; underline "$1"; } + +# SOQL queries for the two main license types that exist in most orgs +SOQL_USER='SELECT Id, Name, TotalLicenses, UsedLicenses FROM UserLicense ORDER BY Name LIMIT 200' +SOQL_PSL='SELECT Id, MasterLabel, DeveloperName, TotalLicenses, UsedLicenses FROM PermissionSetLicense ORDER BY MasterLabel LIMIT 200' + +# Validate org before proceeding +validate_org "$ORG" + +header + +# 1) User Licenses - Core Salesforce licenses +section "User Licenses" +if JUSER="$(run_json_with_error_handling "$SOQL_USER")"; then + if ! echo "$JUSER" | jq -e . >/dev/null 2>&1; then + echo "Failed to query UserLicense (no valid JSON)." + echo + else + print_table_or_empty "$JUSER" \ + '[ .Name, + (try .TotalLicenses|tonumber // 0), + (try .UsedLicenses|tonumber // 0), + ((try .TotalLicenses|tonumber // 0) - (try .UsedLicenses|tonumber // 0)) + ]' \ + "Name,Total,Used,Remaining" + print_totals "$JUSER" + fi +elif [[ $? -eq 42 ]]; then + echo "UserLicense object not available in this org." + echo +fi + +# 2) Permission Set Licenses - Add-on feature licenses +section "Permission Set Licenses" +if JPSL="$(run_json_with_error_handling "$SOQL_PSL")"; then + if ! echo "$JPSL" | jq -e . >/dev/null 2>&1; then + echo "Failed to query PermissionSetLicense (no valid JSON)." + echo + else + print_table_or_empty "$JPSL" \ + '[ .MasterLabel, + .DeveloperName, + (try .TotalLicenses|tonumber // 0), + (try .UsedLicenses|tonumber // 0), + ((try .TotalLicenses|tonumber // 0) - (try .UsedLicenses|tonumber // 0)) + ]' \ + "MasterLabel,DeveloperName,Total,Used,Remaining" + print_totals "$JPSL" + fi +elif [[ $? -eq 42 ]]; then + echo "PermissionSetLicense object not available in this org." + echo +fi + +echo -e "${DIM}Note: This report covers the main Salesforce license types available in most orgs.${RESET}" +echo -e "${DIM}FeatureLicense objects are not commonly available and have been excluded.${RESET}"