Skip to content

Instantly share code, notes, and snippets.

@jeffreyolchovy
Last active March 12, 2026 00:08
Show Gist options
  • Select an option

  • Save jeffreyolchovy/0d0c63ef97c0599de0e0a668bbc2b3f9 to your computer and use it in GitHub Desktop.

Select an option

Save jeffreyolchovy/0d0c63ef97c0599de0e0a668bbc2b3f9 to your computer and use it in GitHub Desktop.
Transfer Gemini Enterprise license seats between GCP projects
# =============================================================================
# Gemini Enterprise License Transfer - Configuration
# =============================================================================
# Copy this file to .env and fill in your values:
# cp .env.example .env
#
# Then run:
# ./transfer-licenses.sh # execute the transfer
# ./transfer-licenses.sh --dry-run # preview the curl commands without executing
# =============================================================================
# ---------------------------------------------------------------------------
# Billing Account (found on Gemini Enterprise "Manage subscription" page)
# ---------------------------------------------------------------------------
# Navigate to: Manage subscription -> select billing account -> click subscription
# The detail page shows both BILLING_ACCOUNT_ID and BILLING_ACCOUNT_LICENSE_CONFIG_ID
BILLING_ACCOUNT_ID=
BILLING_ACCOUNT_LICENSE_CONFIG_ID=
# ---------------------------------------------------------------------------
# Source Project (retract licenses FROM this project)
# ---------------------------------------------------------------------------
SOURCE_PROJECT_NUMBER=
# ---------------------------------------------------------------------------
# Target Project (distribute licenses TO this project)
# ---------------------------------------------------------------------------
TARGET_PROJECT_NUMBER=
# ---------------------------------------------------------------------------
# Quota Project
# ---------------------------------------------------------------------------
# The project number where you have the "Service Usage Consumer" role.
# This can be the same as TARGET_PROJECT_NUMBER if you have the necessary
# permissions on that project. Note: use the literal value, not a variable reference.
MY_PROJECT_NUMBER=
# ---------------------------------------------------------------------------
# Location
# ---------------------------------------------------------------------------
# "us" -> US multi-region (endpoint prefix: us-)
# "eu" -> EU multi-region (endpoint prefix: eu-)
# "global" -> Global multi-region (no endpoint prefix)
LOCATION=global
# ---------------------------------------------------------------------------
# License Config IDs (found on "Manage users" page of Gemini Enterprise UI)
# ---------------------------------------------------------------------------
# The license config ID on the SOURCE project (required for retract step).
SOURCE_LICENSE_CONFIG_ID=
# The license config ID on the TARGET project (optional for distribute step).
# Leave empty to auto-create a new LicenseConfig on the target project.
DISTRIBUTE_LICENSE_CONFIG_ID=
# ---------------------------------------------------------------------------
# License Count
# ---------------------------------------------------------------------------
# Number of license seats to transfer. Use 1 for testing.
LICENSE_COUNT=1
#!/usr/bin/env bash
# =============================================================================
# transfer-licenses.sh
# Gemini Enterprise License Transfer between GCP Projects
#
# Retracts license seats from a source project back to a billing account,
# then distributes them to a target project.
#
# Usage:
# ./transfer-licenses.sh # execute the transfer
# ./transfer-licenses.sh --dry-run # preview curl commands without executing
# ./transfer-licenses.sh --help # show usage
# =============================================================================
set -euo pipefail
# ------------------------------- constants -----------------------------------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${SCRIPT_DIR}/.env"
API_VERSION="v1alpha"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'
# ------------------------------- helpers -------------------------------------
info() { printf "${CYAN}[INFO]${RESET} %s\n" "$*"; }
success() { printf "${GREEN}[OK]${RESET} %s\n" "$*"; }
warn() { printf "${YELLOW}[WARN]${RESET} %s\n" "$*"; }
error() { printf "${RED}[ERROR]${RESET} %s\n" "$*" >&2; }
fatal() { error "$@"; exit 1; }
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS]
Transfer Gemini Enterprise license seats between GCP projects.
Options:
--retract-only Only retract licenses from the source project.
--distribute-only Only distribute licenses to the target project.
--dry-run Print the curl commands without executing them.
--help Show this help message and exit.
If neither --retract-only nor --distribute-only is specified, both steps
are run in sequence (retract then distribute).
Configuration:
All parameters are read from ${ENV_FILE}.
Copy .env.example to .env and fill in your values before running.
Environment variables override values in .env.
EOF
}
# ------------------------------- parse flags ---------------------------------
DRY_RUN=false
RUN_RETRACT=true
RUN_DISTRIBUTE=true
MODE_EXPLICIT=false
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=true; shift ;;
--retract-only) RUN_RETRACT=true; RUN_DISTRIBUTE=false; MODE_EXPLICIT=true; shift ;;
--distribute-only) RUN_RETRACT=false; RUN_DISTRIBUTE=true; MODE_EXPLICIT=true; shift ;;
--help|-h) usage; exit 0 ;;
*) fatal "Unknown option: $1. Use --help for usage." ;;
esac
done
# ------------------------------- load .env -----------------------------------
if [[ ! -f "$ENV_FILE" ]]; then
fatal ".env file not found at ${ENV_FILE}\n Run: cp .env.example .env and fill in your values."
fi
# Source the env file, but do NOT overwrite variables already set in the
# environment. This lets callers override any value:
# SOURCE_PROJECT_NUMBER=12345 ./transfer-licenses.sh
while IFS='=' read -r key value; do
# Skip blank lines and comments
[[ -z "$key" || "$key" =~ ^[[:space:]]*# ]] && continue
# Trim leading/trailing whitespace from key
key=$(echo "$key" | xargs)
# Strip surrounding quotes (single or double) and whitespace from value
value=$(echo "$value" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/^["'\'']//' -e 's/["'\''"]$//')
# Only set the variable if it is not already present in the environment
if [[ -z "${!key+x}" ]]; then
export "$key=$value"
fi
done < "$ENV_FILE"
# ------------------------------- validate ------------------------------------
REQUIRED_VARS=(
BILLING_ACCOUNT_ID
BILLING_ACCOUNT_LICENSE_CONFIG_ID
MY_PROJECT_NUMBER
LICENSE_COUNT
)
# Add mode-specific required vars
if $RUN_RETRACT; then
REQUIRED_VARS+=(SOURCE_PROJECT_NUMBER SOURCE_LICENSE_CONFIG_ID)
fi
if $RUN_DISTRIBUTE; then
REQUIRED_VARS+=(TARGET_PROJECT_NUMBER)
fi
missing=()
for var in "${REQUIRED_VARS[@]}"; do
if [[ -z "${!var:-}" ]]; then
missing+=("$var")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
fatal "Missing required variables in .env:\n $(printf '%s\n ' "${missing[@]}")"
fi
# LOCATION is allowed to be empty (means Global)
LOCATION="${LOCATION:-}"
# ------------------------------- derived values ------------------------------
# Derive the endpoint location prefix from LOCATION
case "${LOCATION}" in
us) ENDPOINT_LOCATION="us-" ;;
eu) ENDPOINT_LOCATION="eu-" ;;
""|global) ENDPOINT_LOCATION="" ;;
*) fatal "Invalid LOCATION '${LOCATION}'. Must be 'us', 'eu', 'global', or empty." ;;
esac
BASE_URL="https://${ENDPOINT_LOCATION}discoveryengine.googleapis.com/${API_VERSION}"
BILLING_BASE="${BASE_URL}/billingAccounts/${BILLING_ACCOUNT_ID}/billingAccountLicenseConfigs/${BILLING_ACCOUNT_LICENSE_CONFIG_ID}"
# ------------------------------- pre-flight ----------------------------------
info "Running pre-flight checks..."
command -v gcloud >/dev/null 2>&1 || fatal "'gcloud' not found. Install the Google Cloud SDK first."
command -v curl >/dev/null 2>&1 || fatal "'curl' not found."
command -v jq >/dev/null 2>&1 || warn "'jq' not found. Responses will not be pretty-printed."
# Verify we have a valid access token
if ! gcloud auth print-access-token >/dev/null 2>&1; then
warn "No active gcloud credentials. Launching interactive login..."
gcloud auth login --no-launch-browser || fatal "Authentication failed."
fi
success "gcloud authentication OK"
# ------------------------------- summary -------------------------------------
echo ""
printf "${BOLD}╔══════════════════════════════════════════════════════════════╗${RESET}\n"
printf "${BOLD}║ Gemini Enterprise License Transfer ║${RESET}\n"
printf "${BOLD}╚══════════════════════════════════════════════════════════════╝${RESET}\n"
echo ""
printf " Billing Account ID: %s\n" "$BILLING_ACCOUNT_ID"
printf " Billing License Config ID: %s\n" "$BILLING_ACCOUNT_LICENSE_CONFIG_ID"
if $RUN_RETRACT; then
printf " Source Project Number: %s\n" "$SOURCE_PROJECT_NUMBER"
printf " Source License Config ID: %s\n" "$SOURCE_LICENSE_CONFIG_ID"
fi
if $RUN_DISTRIBUTE; then
printf " Target Project Number: %s\n" "$TARGET_PROJECT_NUMBER"
printf " Distribute License Config ID: %s\n" "${DISTRIBUTE_LICENSE_CONFIG_ID:-"(auto-create)"}"
fi
printf " Quota Project (MY_PROJECT): %s\n" "$MY_PROJECT_NUMBER"
printf " Location: %s\n" "${LOCATION:-"(global)"}"
printf " Endpoint Location Prefix: %s\n" "${ENDPOINT_LOCATION:-"(none)"}"
printf " License Count: %s\n" "$LICENSE_COUNT"
echo ""
# Show mode
if $RUN_RETRACT && $RUN_DISTRIBUTE; then
printf " ${CYAN}Mode: Retract + Distribute${RESET}\n"
elif $RUN_RETRACT; then
printf " ${CYAN}Mode: Retract only${RESET}\n"
else
printf " ${CYAN}Mode: Distribute only${RESET}\n"
fi
if $DRY_RUN; then
printf " ${YELLOW}DRY RUN (no API calls will be made)${RESET}\n"
fi
echo ""
# ------------------------------- confirm -------------------------------------
read -r -p "Proceed with the transfer? [y/N] " confirm
case "$confirm" in
[yY]|[yY][eE][sS]) ;;
*) info "Aborted."; exit 0 ;;
esac
echo ""
# ------------------------------- execute API call ----------------------------
# Runs a curl command and handles the response.
# Arguments:
# $1 - step label (e.g. "Retract" or "Distribute")
# $2 - URL
# $3 - JSON body
execute_api_call() {
local label="$1"
local url="$2"
local body="$3"
echo "---"
info "${label}: URL"
echo " ${url}"
echo ""
info "${label}: Request Body"
if command -v jq >/dev/null 2>&1; then
echo "$body" | jq .
else
echo " ${body}"
fi
echo ""
if $DRY_RUN; then
info "${label}: curl command (dry run)"
cat <<CURL_CMD
curl -X POST \\
-H "Authorization: Bearer \$(gcloud auth print-access-token)" \\
-H "Content-Type: application/json" \\
-H "X-Goog-User-Project: ${MY_PROJECT_NUMBER}" \\
-d '${body}' \\
"${url}"
CURL_CMD
echo ""
success "${label}: Dry run complete (no request sent)"
return 0
fi
local http_code
local response
response=$(curl -s -w "\n%{http_code}" -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json" \
-H "X-Goog-User-Project: ${MY_PROJECT_NUMBER}" \
-d "${body}" \
"${url}")
# The last line is the HTTP status code
http_code=$(echo "$response" | tail -n1)
response=$(echo "$response" | sed '$d')
info "${label}: HTTP ${http_code}"
if command -v jq >/dev/null 2>&1; then
echo "$response" | jq . 2>/dev/null || echo "$response"
else
echo "$response"
fi
echo ""
if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then
success "${label}: Completed successfully (HTTP ${http_code})"
return 0
else
error "${label}: Failed with HTTP ${http_code}"
return 1
fi
}
# ------------------------------- Step 1: Retract -----------------------------
retract_ok=true
if $RUN_RETRACT; then
RETRACT_URL="${BILLING_BASE}:retractLicenseConfig"
RETRACT_BODY=$(cat <<JSON
{
"licenseConfig": "projects/${SOURCE_PROJECT_NUMBER}/locations/${LOCATION}/licenseConfigs/${SOURCE_LICENSE_CONFIG_ID}",
"licenseCount": ${LICENSE_COUNT}
}
JSON
)
if $RUN_DISTRIBUTE; then
info "Step 1/2: Retracting ${LICENSE_COUNT} license(s) from project ${SOURCE_PROJECT_NUMBER}..."
else
info "Retracting ${LICENSE_COUNT} license(s) from project ${SOURCE_PROJECT_NUMBER}..."
fi
echo ""
execute_api_call "Retract" "$RETRACT_URL" "$RETRACT_BODY" || retract_ok=false
echo ""
# Ask whether to continue if retract failed and distribute is also planned
if $RUN_DISTRIBUTE && ! $DRY_RUN && ! $retract_ok; then
warn "Retract step failed. The distribute step may still work if the licenses are already at the billing account level."
read -r -p "Continue with distribute? [y/N] " cont
case "$cont" in
[yY]|[yY][eE][sS]) ;;
*) fatal "Aborted after retract failure." ;;
esac
echo ""
fi
fi
# ------------------------------- Step 2: Distribute --------------------------
distribute_ok=true
if $RUN_DISTRIBUTE; then
DISTRIBUTE_URL="${BILLING_BASE}:distributeLicenseConfig"
# Build the distribute JSON body; omit licenseConfigId if not specified
if [[ -n "${DISTRIBUTE_LICENSE_CONFIG_ID:-}" ]]; then
DISTRIBUTE_BODY=$(cat <<JSON
{
"projectNumber": "${TARGET_PROJECT_NUMBER}",
"location": "${LOCATION}",
"licenseCount": ${LICENSE_COUNT},
"licenseConfigId": "${DISTRIBUTE_LICENSE_CONFIG_ID}"
}
JSON
)
else
DISTRIBUTE_BODY=$(cat <<JSON
{
"projectNumber": "${TARGET_PROJECT_NUMBER}",
"location": "${LOCATION}",
"licenseCount": ${LICENSE_COUNT}
}
JSON
)
fi
if $RUN_RETRACT; then
info "Step 2/2: Distributing ${LICENSE_COUNT} license(s) to project ${TARGET_PROJECT_NUMBER}..."
else
info "Distributing ${LICENSE_COUNT} license(s) to project ${TARGET_PROJECT_NUMBER}..."
fi
echo ""
execute_api_call "Distribute" "$DISTRIBUTE_URL" "$DISTRIBUTE_BODY" || distribute_ok=false
fi
# ------------------------------- Summary -------------------------------------
echo ""
printf "${BOLD}══════════════════════════════════════════════════════════════${RESET}\n"
printf "${BOLD} Summary${RESET}\n"
printf "${BOLD}══════════════════════════════════════════════════════════════${RESET}\n"
if $DRY_RUN; then
printf " ${YELLOW}DRY RUN — no API calls were made${RESET}\n"
fi
if $RUN_RETRACT; then
if $retract_ok; then
printf " Retract: ${GREEN}OK${RESET}\n"
else
printf " Retract: ${RED}FAILED${RESET}\n"
fi
fi
if $RUN_DISTRIBUTE; then
if $distribute_ok; then
printf " Distribute: ${GREEN}OK${RESET}\n"
else
printf " Distribute: ${RED}FAILED${RESET}\n"
fi
fi
echo ""
all_ok=true
$RUN_RETRACT && ! $retract_ok && all_ok=false
$RUN_DISTRIBUTE && ! $distribute_ok && all_ok=false
if $all_ok; then
success "License transfer complete!"
exit 0
else
error "One or more steps failed. Review the output above."
exit 1
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment