#!/bin/bash # ods-usage - calculate On-Demand Sandbox credits used from a getRealmUsage response # # Usage: # ods-usage [api_response] # ods-usage -h # # Options: # api_response JSON response from getRealmUsage (ODS API); if omitted, # the script pulls JSON from the clipboard via pbpaste # -h, --help Show help message # # Example Output: # Sandbox Counts # created: 9 # active: 8 # deleted: 0 # Uptime & Downtime # min up: 1389 # > medium: 1389 # min down: 194006 # Credits Used # up: 1389 # down: 58201 # total: 59590 # # References: # https://admin.dx.commercecloud.salesforce.com/#/Realms/getRealmUsage # https://admin.dx.commercecloud.salesforce.com/api/v1/realms/xxxx/usage?from=2020-01-01&to=2020-02-01&detailedReport=false _ods_usage() ( local SCRIPT_NAME; SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")" case "${BASH_SOURCE[0]}" in /dev/*|/proc/*) SCRIPT_NAME="" ;; esac case "$SCRIPT_NAME" in ""|bash|sh|zsh|dash) SCRIPT_NAME="ods-usage" ;; esac _error() { echo "[ERR][$SCRIPT_NAME] $*" >&2; } _terminal_cols() { # Returns terminal column count (integer), falling back to 80 # Prefers stty against /dev/tty (real ioctl-sourced width), then $COLUMNS # Avoids tput to drop the runtime dependency # The { ...; } 2>/dev/null wrapping silences the shell's own redirect # error ("/dev/tty: Device not configured") when /dev/tty is unavailable local cols="" local out="" if out="$({ stty size /dev/null)"; then cols="${out##* }" case "$cols" in ''|*[!0-9]*) cols="" ;; esac [ "$cols" ] && [ "$cols" -gt 0 ] 2>/dev/null && { printf '%s\n' "$cols"; return 0; } fi cols="${COLUMNS:-}" case "$cols" in ''|*[!0-9]*) cols="" ;; esac [ "$cols" ] && [ "$cols" -gt 0 ] 2>/dev/null && { printf '%s\n' "$cols"; return 0; } printf '80\n' } _show_help() { local s; [ -t 1 ] && s=$'\033[4m' local r; [ -t 1 ] && r=$'\033[24m' echo "NAME" echo " $SCRIPT_NAME - calculate On-Demand Sandbox credits used from an ODS API response" echo "SYNOPSIS" echo " $SCRIPT_NAME [${s}api_response${r}]" echo " $SCRIPT_NAME -h" echo "DESCRIPTION" echo " Parses a getRealmUsage JSON response from the Salesforce B2C Commerce" echo " On-Demand Sandbox (ODS) API and summarizes sandbox counts, uptime/" echo " downtime by profile, and credits used. Credit multipliers:" echo " medium=1, large=2, xlarge=4 (per minute up)" echo " stopped=0.3 (per minute down, all profiles)" echo "" echo " Primary workflow: copy the JSON response from the ODS API Swagger UI" echo " (https://admin.dx.commercecloud.salesforce.com/#/Realms/getRealmUsage)," echo " then run \`$SCRIPT_NAME\` with no arguments -- it will read JSON from" echo " the clipboard via pbpaste. You can also pass the JSON as an argument." echo "INPUT" echo " Expects JSON with these required fields under .data:" echo " createdSandboxes, activeSandboxes, deletedSandboxes," echo " minutesUp, minutesDown, minutesUpByProfile[]" echo "OPTIONS" echo " ${s}api_response${r} JSON response (if omitted, read from clipboard via pbpaste)" echo " -h, --help Show this help message" echo "DEPENDENCIES" echo " jq, pbpaste (macOS clipboard; required for clipboard mode)" echo "EXAMPLES" echo " $SCRIPT_NAME '{\"data\":{\"createdSandboxes\":9,...}}'" echo " $SCRIPT_NAME # reads JSON from clipboard (macOS)" echo " pbpaste | xargs -0 $SCRIPT_NAME # explicit clipboard pipe" echo "EXIT STATUS" echo " 0 Success" echo " 2 Usage error (invalid JSON, missing required fields, no input)" echo " 3 Dependency error (jq missing; pbpaste missing in clipboard mode)" } case "$1" in -h|--help) _show_help; return 0 ;; esac if ! command -v jq >/dev/null 2>&1; then _error "jq is required" return 3 fi # Define one of two versions of a jq helper function, to pipe the input JSON and specify raw output each time # Helper will use input if arguments are provided, otherwise it will pull from the clipboard if [ $# -gt 0 ]; then local json="$*" # Define jq helper if input is valid JSON, otherwise exit with error status if echo "$json" | jq empty 2>/dev/null; then _jq() { echo "$json" | jq -r "$@"; } else _error "Input is not valid JSON (first line preview below). Run \`$SCRIPT_NAME -h\` for usage" local width; width="$(_terminal_cols)" echo "$*" | head -n 1 | cut -c "1-$width" >&2 return 2 fi else if ! command -v pbpaste >/dev/null 2>&1; then _error "pbpaste is required for clipboard mode (macOS only). Pass JSON as an argument instead" return 3 fi # Define jq helper if clipboard contains valid JSON, otherwise exit with error status if pbpaste | jq empty 2>/dev/null; then _jq() { pbpaste | jq -r "$@"; } else _error "No arguments provided and clipboard does not contain valid JSON. Run \`$SCRIPT_NAME -h\` for usage" return 2 fi fi # Validate required fields exist before processing -- missing fields would # otherwise print "null" and leak jq errors to stderr while exiting 0 local check; check="$(_jq '.data | has("createdSandboxes") and has("activeSandboxes") and has("deletedSandboxes") and has("minutesUp") and has("minutesDown")')" if [ "$check" != "true" ]; then _error "Input JSON is missing required fields (expected .data.createdSandboxes, .data.activeSandboxes, .data.deletedSandboxes, .data.minutesUp, .data.minutesDown). Run \`$SCRIPT_NAME -h\` for usage" return 2 fi # If output is being piped or redirected, do not print control characters for text formatting local smul='' local rmul='' if [ -t 1 ]; then # stdout (fd1) is a terminal smul=$'\033[4m' # start underline rmul=$'\033[24m' # remove underline fi # Define credit cost per minute for each ODS resource profile (all profiles are 0.3 credits per minute down) local credit_multi_medium=1 local credit_multi_large=2 local credit_multi_xlarge=4 local credit_multi_stopped="3 / 10" # injected into $(()) later, because $((3 / 10)) == 0 # Print the number of sandboxes created, active, and deleted during the timeframe echo "${smul}Sandbox Counts${rmul}" local sbx_count_created; sbx_count_created="$(_jq '.data.createdSandboxes')" local sbx_count_active; sbx_count_active="$(_jq '.data.activeSandboxes')" local sbx_count_deleted; sbx_count_deleted="$(_jq '.data.deletedSandboxes')" echo "created: $sbx_count_created" echo "active: $sbx_count_active" echo "deleted: $sbx_count_deleted" # Print minutes up and down (per resource profile for uptime only, as it does not apply to downtime) echo "${smul}Uptime & Downtime${rmul}" local min_down; min_down="$(_jq '.data.minutesDown')" local min_up; min_up="$(_jq '.data.minutesUp')" local min_up_medium; min_up_medium="$(_jq '.data.minutesUpByProfile[] | select(.profile=="medium") | .minutes')" local min_up_large; min_up_large="$(_jq '.data.minutesUpByProfile[] | select(.profile=="large") | .minutes')" local min_up_xlarge; min_up_xlarge="$(_jq '.data.minutesUpByProfile[] | select(.profile=="xlarge") | .minutes')" echo "min up: $min_up" [ "$min_up_medium" ] && echo "> medium: $min_up_medium" [ "$min_up_large" ] && echo "> large: $min_up_large" [ "$min_up_xlarge" ] && echo "> xlarge: $min_up_xlarge" echo "min down: $min_down" # Print credits used by uptime, downtime, and in total echo "${smul}Credits Used${rmul}" local credits_used_up=$((min_up_medium*credit_multi_medium + min_up_large*credit_multi_large + min_up_xlarge*credit_multi_xlarge)) local credits_used_down; credits_used_down="$(eval 'echo $((min_down * '"$credit_multi_stopped"'))')" local credits_used_total=$((credits_used_up + credits_used_down)) echo "up: $credits_used_up" echo "down: $credits_used_down" echo "total: $credits_used_total" ) _ods_usage "$@" __ods_usage_rc=$? unset -f _ods_usage if [ -n "${BASH_SOURCE[0]}" ] && [ "${BASH_SOURCE[0]}" != "$0" ]; then eval "unset __ods_usage_rc; return $__ods_usage_rc" fi eval "unset __ods_usage_rc; exit $__ods_usage_rc"