a1a17e81a1
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
671 lines
26 KiB
Bash
671 lines
26 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
#########################################################################################
|
|
#### ami-lifecycle-manager.sh — AWS AMI lifecycle management ####
|
|
#### Create, tag, retain, and deregister AMIs with orphan snapshot cleanup ####
|
|
#### Requires: bash 4+, aws-cli v2, jq ####
|
|
#### ####
|
|
#### Author: Phil Connor ####
|
|
#### Contact: contact@mylinux.work ####
|
|
#### License: MIT ####
|
|
#### Version 1.00 ####
|
|
#### ####
|
|
#### Usage: ####
|
|
#### ./ami-lifecycle-manager.sh --create --instance-id i-1234567890abcdef0 ####
|
|
#### ./ami-lifecycle-manager.sh --enforce --retention-days 30 ####
|
|
#### ####
|
|
#### See --help for all options. ####
|
|
#########################################################################################
|
|
|
|
set -euo pipefail
|
|
|
|
# ── Defaults ──────────────────────────────────────────────────────────
|
|
AWS_REGION="${AWS_REGION:-}"
|
|
AWS_PROFILE_OPT="${AWS_PROFILE:-}"
|
|
AMI_RETENTION_DAYS="${AMI_RETENTION_DAYS:-30}"
|
|
AMI_NAME_PREFIX="${AMI_NAME_PREFIX:-ami-lifecycle}"
|
|
VERBOSE="${VERBOSE:-false}"
|
|
COLOR="${COLOR:-auto}"
|
|
DRY_RUN="false"
|
|
OUTPUT_FORMAT="${OUTPUT_FORMAT:-text}"
|
|
|
|
# ── State ─────────────────────────────────────────────────────────────
|
|
SCRIPT_NAME="$(basename "$0")"
|
|
readonly SCRIPT_NAME
|
|
RUN_MODE=""
|
|
INSTANCE_ID=""
|
|
RETENTION_DAYS="$AMI_RETENTION_DAYS"
|
|
START_TIME=""
|
|
MANAGER_TAG="ami-lifecycle-manager"
|
|
|
|
# ── Colors ────────────────────────────────────────────────────────────
|
|
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" DIM="" RESET=""
|
|
setup_colors() {
|
|
if [[ "$COLOR" == "never" ]]; then
|
|
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" DIM="" RESET=""
|
|
return
|
|
fi
|
|
if [[ "$COLOR" == "auto" && ! -t 1 ]]; then
|
|
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" DIM="" RESET=""
|
|
return
|
|
fi
|
|
RED="\033[0;31m"
|
|
GREEN="\033[0;32m"
|
|
YELLOW="\033[0;33m"
|
|
BLUE="\033[0;34m"
|
|
BOLD="\033[1m"
|
|
DIM="\033[2m"
|
|
RESET="\033[0m"
|
|
}
|
|
|
|
# ── Logging ───────────────────────────────────────────────────────────
|
|
log_info() { printf "${GREEN}[INFO]${RESET} %s\n" "$*"; }
|
|
log_warn() { printf "${YELLOW}[WARN]${RESET} %s\n" "$*" >&2; }
|
|
log_error() { printf "${RED}[ERROR]${RESET} %s\n" "$*" >&2; }
|
|
log_verbose() { [[ "$VERBOSE" == "true" ]] && printf "${DIM}[DEBUG] %s${RESET}\n" "$*"; }
|
|
|
|
# ── Helpers ───────────────────────────────────────────────────────────
|
|
die() { log_error "$@"; exit 1; }
|
|
|
|
today_utc() { date -u +%Y-%m-%d; }
|
|
|
|
epoch_from_date() {
|
|
local d="$1"
|
|
if date --version >/dev/null 2>&1; then
|
|
date -d "$d" +%s
|
|
else
|
|
date -j -f "%Y-%m-%d" "$d" +%s
|
|
fi
|
|
}
|
|
|
|
days_since() {
|
|
local created="$1"
|
|
local now
|
|
now=$(date -u +%s)
|
|
local then
|
|
then=$(epoch_from_date "$created")
|
|
echo $(( (now - then) / 86400 ))
|
|
}
|
|
|
|
date_offset_days() {
|
|
local base="$1" offset="$2"
|
|
if date --version >/dev/null 2>&1; then
|
|
date -d "${base} +${offset} days" +%Y-%m-%d
|
|
else
|
|
date -j -v+"${offset}d" -f "%Y-%m-%d" "$base" +%Y-%m-%d
|
|
fi
|
|
}
|
|
|
|
# ── AWS CLI wrapper ───────────────────────────────────────────────────
|
|
aws_cmd() {
|
|
local args=("$@")
|
|
[[ -n "$AWS_REGION" ]] && args+=(--region "$AWS_REGION")
|
|
[[ -n "$AWS_PROFILE_OPT" ]] && args+=(--profile "$AWS_PROFILE_OPT")
|
|
log_verbose "aws ${args[*]}"
|
|
aws "${args[@]}"
|
|
}
|
|
|
|
# ── Dependency check ─────────────────────────────────────────────────
|
|
check_deps() {
|
|
local missing=()
|
|
command -v aws >/dev/null 2>&1 || missing+=("aws-cli")
|
|
command -v jq >/dev/null 2>&1 || missing+=("jq")
|
|
if (( ${#missing[@]} > 0 )); then
|
|
die "Missing required tools: ${missing[*]}"
|
|
fi
|
|
|
|
local bash_major="${BASH_VERSINFO[0]}"
|
|
if (( bash_major < 4 )); then
|
|
die "Requires bash 4+, found ${BASH_VERSION}"
|
|
fi
|
|
|
|
# Verify AWS credentials
|
|
if ! aws_cmd sts get-caller-identity --output text >/dev/null 2>&1; then
|
|
die "AWS credentials not configured or expired"
|
|
fi
|
|
|
|
# Determine region
|
|
if [[ -z "$AWS_REGION" ]]; then
|
|
AWS_REGION=$(aws configure get region 2>/dev/null || echo "")
|
|
if [[ -z "$AWS_REGION" ]]; then
|
|
die "AWS_REGION is required (set via env var, --region, or aws configure)"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# ── Header ────────────────────────────────────────────────────────────
|
|
print_header() {
|
|
local account_id
|
|
account_id=$(aws_cmd sts get-caller-identity --query Account --output text 2>/dev/null || echo "unknown")
|
|
|
|
echo "AMI Lifecycle Manager"
|
|
echo "Account: $account_id"
|
|
echo "Region: $AWS_REGION"
|
|
echo "Mode: $RUN_MODE"
|
|
echo "Time: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
echo ""
|
|
}
|
|
|
|
# ── Usage ─────────────────────────────────────────────────────────────
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $SCRIPT_NAME [MODE] [OPTIONS]
|
|
|
|
Modes:
|
|
--create Create an AMI from the specified instance
|
|
--enforce Enforce retention policy — deregister expired AMIs
|
|
--clean-snapshots Delete orphaned snapshots from deregistered AMIs
|
|
--inventory List all managed AMIs with age and retention info
|
|
|
|
Options:
|
|
--instance-id ID EC2 instance ID (required for --create)
|
|
--retention-days N Retention period in days (default: $AMI_RETENTION_DAYS)
|
|
--dry-run Preview destructive operations without executing
|
|
--format FORMAT text (default), csv, json
|
|
--profile PROFILE AWS CLI profile
|
|
--region REGION AWS region
|
|
--verbose Debug output
|
|
--no-color Disable colored output
|
|
--help Show this help
|
|
|
|
Environment variables:
|
|
AWS_PROFILE AWS CLI profile
|
|
AWS_REGION AWS region
|
|
AMI_RETENTION_DAYS Default retention (default: 30)
|
|
AMI_NAME_PREFIX AMI name prefix (default: ami-lifecycle)
|
|
EOF
|
|
exit 0
|
|
}
|
|
|
|
# ── Argument parsing ──────────────────────────────────────────────────
|
|
parse_args() {
|
|
while (( $# > 0 )); do
|
|
case "$1" in
|
|
--create)
|
|
RUN_MODE="create"; shift ;;
|
|
--enforce)
|
|
RUN_MODE="enforce"; shift ;;
|
|
--clean-snapshots)
|
|
RUN_MODE="clean-snapshots"; shift ;;
|
|
--inventory)
|
|
RUN_MODE="inventory"; shift ;;
|
|
--instance-id)
|
|
[[ $# -lt 2 ]] && die "--instance-id requires a value"
|
|
INSTANCE_ID="$2"; shift 2 ;;
|
|
--retention-days)
|
|
[[ $# -lt 2 ]] && die "--retention-days requires a value"
|
|
RETENTION_DAYS="$2"; shift 2 ;;
|
|
--dry-run)
|
|
DRY_RUN="true"; shift ;;
|
|
--format)
|
|
[[ $# -lt 2 ]] && die "--format requires a value"
|
|
OUTPUT_FORMAT="$2"; shift 2 ;;
|
|
--profile)
|
|
[[ $# -lt 2 ]] && die "--profile requires a value"
|
|
AWS_PROFILE_OPT="$2"; shift 2 ;;
|
|
--region)
|
|
[[ $# -lt 2 ]] && die "--region requires a value"
|
|
AWS_REGION="$2"; shift 2 ;;
|
|
--verbose)
|
|
VERBOSE="true"; shift ;;
|
|
--no-color)
|
|
COLOR="never"; shift ;;
|
|
--help|-h)
|
|
usage ;;
|
|
*)
|
|
die "Unknown option: $1 (see --help)" ;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$RUN_MODE" ]]; then
|
|
log_error "No mode specified"
|
|
echo ""
|
|
usage
|
|
fi
|
|
|
|
if [[ "$RUN_MODE" == "create" && -z "$INSTANCE_ID" ]]; then
|
|
die "--create requires --instance-id"
|
|
fi
|
|
|
|
case "$OUTPUT_FORMAT" in
|
|
text|csv|json) ;;
|
|
*) die "Invalid --format: $OUTPUT_FORMAT (expected text, csv, json)" ;;
|
|
esac
|
|
|
|
if ! [[ "$RETENTION_DAYS" =~ ^[0-9]+$ ]]; then
|
|
die "--retention-days must be a positive integer"
|
|
fi
|
|
}
|
|
|
|
# ── Get instance name ─────────────────────────────────────────────────
|
|
get_instance_name() {
|
|
local iid="$1"
|
|
aws_cmd ec2 describe-instances \
|
|
--instance-ids "$iid" \
|
|
--query 'Reservations[0].Instances[0].Tags[?Key==`Name`].Value | [0]' \
|
|
--output text 2>/dev/null || echo "N/A"
|
|
}
|
|
|
|
# ── Create AMI ────────────────────────────────────────────────────────
|
|
create_ami() {
|
|
local instance_id="$INSTANCE_ID"
|
|
local today
|
|
today="$(today_utc)"
|
|
|
|
log_info "Creating AMI from instance ${instance_id}..."
|
|
|
|
# Get instance name
|
|
local instance_name
|
|
instance_name=$(get_instance_name "$instance_id")
|
|
if [[ "$instance_name" == "None" || -z "$instance_name" ]]; then
|
|
instance_name="unnamed"
|
|
fi
|
|
log_info "Instance name: ${instance_name}"
|
|
|
|
# Build AMI name
|
|
local ami_name="${AMI_NAME_PREFIX}-${instance_name}-${today}"
|
|
local ami_description="AMI created by ${MANAGER_TAG} from ${instance_id} (${instance_name}) on ${today}"
|
|
|
|
# Calculate expiry date
|
|
local expires
|
|
expires=$(date_offset_days "$today" "$RETENTION_DAYS")
|
|
|
|
# Create the AMI (no-reboot to avoid downtime)
|
|
local ami_id
|
|
ami_id=$(aws_cmd ec2 create-image \
|
|
--instance-id "$instance_id" \
|
|
--name "$ami_name" \
|
|
--description "$ami_description" \
|
|
--no-reboot \
|
|
--query 'ImageId' \
|
|
--output text 2>/dev/null) || die "Failed to create AMI from ${instance_id}"
|
|
|
|
log_info "AMI created: ${ami_id}"
|
|
log_info "Name: ${ami_name}"
|
|
|
|
# Tag the AMI
|
|
aws_cmd ec2 create-tags \
|
|
--resources "$ami_id" \
|
|
--tags \
|
|
"Key=Name,Value=${ami_name}" \
|
|
"Key=managed-by,Value=${MANAGER_TAG}" \
|
|
"Key=source-instance,Value=${instance_id}" \
|
|
"Key=source-name,Value=${instance_name}" \
|
|
"Key=created-date,Value=${today}" \
|
|
"Key=retention-days,Value=${RETENTION_DAYS}" \
|
|
"Key=expires,Value=${expires}" \
|
|
>/dev/null 2>&1 || log_warn "Failed to tag AMI ${ami_id}"
|
|
|
|
log_info "Tags applied:"
|
|
printf " %-16s = %s\n" "managed-by" "$MANAGER_TAG"
|
|
printf " %-16s = %s\n" "source-instance" "$instance_id"
|
|
printf " %-16s = %s\n" "source-name" "$instance_name"
|
|
printf " %-16s = %s\n" "created-date" "$today"
|
|
printf " %-16s = %s\n" "retention-days" "$RETENTION_DAYS"
|
|
printf " %-16s = %s\n" "expires" "$expires"
|
|
|
|
# Wait briefly for snapshots to appear, then tag them too
|
|
log_verbose "Waiting for AMI snapshots to register..."
|
|
local retries=0
|
|
local snap_ids=""
|
|
while (( retries < 12 )); do
|
|
snap_ids=$(aws_cmd ec2 describe-images \
|
|
--image-ids "$ami_id" \
|
|
--query 'Images[0].BlockDeviceMappings[*].Ebs.SnapshotId' \
|
|
--output text 2>/dev/null || echo "")
|
|
if [[ -n "$snap_ids" && "$snap_ids" != "None" ]]; then
|
|
break
|
|
fi
|
|
sleep 5
|
|
((retries++)) || true
|
|
done
|
|
|
|
if [[ -n "$snap_ids" && "$snap_ids" != "None" ]]; then
|
|
for snap_id in $snap_ids; do
|
|
aws_cmd ec2 create-tags \
|
|
--resources "$snap_id" \
|
|
--tags \
|
|
"Key=managed-by,Value=${MANAGER_TAG}" \
|
|
"Key=source-ami,Value=${ami_id}" \
|
|
"Key=source-instance,Value=${instance_id}" \
|
|
"Key=created-date,Value=${today}" \
|
|
>/dev/null 2>&1 || log_warn "Failed to tag snapshot ${snap_id}"
|
|
log_verbose "Tagged snapshot ${snap_id}"
|
|
done
|
|
fi
|
|
}
|
|
|
|
# ── Get managed AMIs ──────────────────────────────────────────────────
|
|
get_managed_amis() {
|
|
local account_id
|
|
account_id=$(aws_cmd sts get-caller-identity --query Account --output text 2>/dev/null)
|
|
|
|
aws_cmd ec2 describe-images \
|
|
--owners "$account_id" \
|
|
--filters "Name=tag:managed-by,Values=${MANAGER_TAG}" \
|
|
--query 'Images[*]' \
|
|
--output json 2>/dev/null || echo "[]"
|
|
}
|
|
|
|
# ── Enforce retention ─────────────────────────────────────────────────
|
|
enforce_retention() {
|
|
log_info "Enforcing retention policy (${RETENTION_DAYS} days)..."
|
|
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
log_info "DRY RUN — no AMIs will be deregistered"
|
|
fi
|
|
|
|
local amis_json
|
|
amis_json=$(get_managed_amis)
|
|
|
|
local total
|
|
total=$(echo "$amis_json" | jq 'length')
|
|
log_info "Found ${total} managed AMI(s)"
|
|
|
|
if (( total == 0 )); then
|
|
log_info "No managed AMIs found — nothing to do"
|
|
return
|
|
fi
|
|
|
|
local today
|
|
today=$(today_utc)
|
|
local active=0 expired=0 deregistered=0
|
|
|
|
# Print table header for text output
|
|
if [[ "$OUTPUT_FORMAT" == "text" ]]; then
|
|
echo ""
|
|
printf " %-24s %-42s %-6s %-11s %s\n" "AMI" "NAME" "AGE" "RETENTION" "STATUS"
|
|
echo " ──────────────────────────────────────────────────────────────────────────────────────"
|
|
fi
|
|
|
|
local csv_lines=()
|
|
local json_items=()
|
|
|
|
while IFS=$'\t' read -r ami_id ami_name created_date retention_tag; do
|
|
[[ -z "$ami_id" || "$ami_id" == "null" ]] && continue
|
|
|
|
# Use tag retention or default
|
|
local ret="${retention_tag}"
|
|
if [[ -z "$ret" || "$ret" == "null" || "$ret" == "None" ]]; then
|
|
ret="$RETENTION_DAYS"
|
|
fi
|
|
|
|
local age=0
|
|
if [[ -n "$created_date" && "$created_date" != "null" && "$created_date" != "None" ]]; then
|
|
age=$(days_since "$created_date")
|
|
fi
|
|
|
|
local status="active"
|
|
if (( age > ret )); then
|
|
status="expired"
|
|
((expired++)) || true
|
|
else
|
|
((active++)) || true
|
|
fi
|
|
|
|
case "$OUTPUT_FORMAT" in
|
|
text)
|
|
local status_icon="✓ active"
|
|
if [[ "$status" == "expired" ]]; then
|
|
status_icon="✗ expired"
|
|
fi
|
|
printf " %-24s %-42s %3dd %3dd %s\n" \
|
|
"$ami_id" "$ami_name" "$age" "$ret" "$status_icon"
|
|
;;
|
|
csv)
|
|
csv_lines+=("\"${ami_id}\",\"${ami_name}\",${age},${ret},\"${status}\"")
|
|
;;
|
|
json)
|
|
json_items+=("{\"ami_id\":\"${ami_id}\",\"name\":\"${ami_name}\",\"age_days\":${age},\"retention_days\":${ret},\"status\":\"${status}\"}")
|
|
;;
|
|
esac
|
|
|
|
# Deregister expired AMIs
|
|
if [[ "$status" == "expired" ]]; then
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
log_info "[DRY RUN] Would deregister ${ami_id} (${age}d old, retention ${ret}d)"
|
|
else
|
|
log_info "Deregistering ${ami_id} (${age}d old, retention ${ret}d)..."
|
|
if aws_cmd ec2 deregister-image --image-id "$ami_id" >/dev/null 2>&1; then
|
|
((deregistered++)) || true
|
|
else
|
|
log_warn "Failed to deregister ${ami_id}"
|
|
fi
|
|
fi
|
|
fi
|
|
done < <(echo "$amis_json" | jq -r '.[] | [.ImageId, (.Tags // [] | map(select(.Key == "Name")) | .[0].Value // "N/A"), (.Tags // [] | map(select(.Key == "created-date")) | .[0].Value // ""), (.Tags // [] | map(select(.Key == "retention-days")) | .[0].Value // "")] | @tsv')
|
|
|
|
echo ""
|
|
|
|
case "$OUTPUT_FORMAT" in
|
|
text)
|
|
echo "Summary"
|
|
printf " Total managed AMIs: %d\n" "$total"
|
|
printf " Active: %d\n" "$active"
|
|
printf " Expired: %d\n" "$expired"
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
printf " Would deregister: %d\n" "$expired"
|
|
else
|
|
printf " Deregistered: %d\n" "$deregistered"
|
|
fi
|
|
;;
|
|
csv)
|
|
echo "ami_id,name,age_days,retention_days,status"
|
|
for line in "${csv_lines[@]}"; do
|
|
echo "$line"
|
|
done
|
|
;;
|
|
json)
|
|
local joined
|
|
joined=$(printf ",%s" "${json_items[@]}")
|
|
joined="${joined:1}"
|
|
printf '{"mode":"enforce","retention_days":%d,"dry_run":%s,"total":%d,"active":%d,"expired":%d,"items":[%s]}\n' \
|
|
"$RETENTION_DAYS" "$DRY_RUN" "$total" "$active" "$expired" "$joined"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ── Clean orphan snapshots ────────────────────────────────────────────
|
|
clean_orphan_snapshots() {
|
|
log_info "Searching for orphaned AMI snapshots..."
|
|
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
log_info "DRY RUN — no snapshots will be deleted"
|
|
fi
|
|
|
|
local account_id
|
|
account_id=$(aws_cmd sts get-caller-identity --query Account --output text 2>/dev/null)
|
|
|
|
# Get all snapshots tagged as managed by us
|
|
local snaps_json
|
|
snaps_json=$(aws_cmd ec2 describe-snapshots \
|
|
--owner-ids "$account_id" \
|
|
--filters "Name=tag:managed-by,Values=${MANAGER_TAG}" \
|
|
--query 'Snapshots[*]' \
|
|
--output json 2>/dev/null) || die "Failed to describe snapshots"
|
|
|
|
local total_snaps
|
|
total_snaps=$(echo "$snaps_json" | jq 'length')
|
|
log_info "Found ${total_snaps} managed snapshot(s)"
|
|
|
|
if (( total_snaps == 0 )); then
|
|
log_info "No managed snapshots found — nothing to do"
|
|
return
|
|
fi
|
|
|
|
# Get all currently registered AMI IDs
|
|
local registered_amis
|
|
registered_amis=$(aws_cmd ec2 describe-images \
|
|
--owners "$account_id" \
|
|
--query 'Images[*].ImageId' \
|
|
--output text 2>/dev/null) || die "Failed to describe images"
|
|
|
|
local orphan_count=0
|
|
local deleted_count=0
|
|
local total_size=0
|
|
|
|
while IFS=$'\t' read -r snap_id source_ami snap_size; do
|
|
[[ -z "$snap_id" || "$snap_id" == "null" ]] && continue
|
|
|
|
# Check if the source AMI still exists
|
|
local is_orphan="false"
|
|
if [[ -z "$source_ami" || "$source_ami" == "null" || "$source_ami" == "None" ]]; then
|
|
is_orphan="true"
|
|
elif ! echo "$registered_amis" | grep -qw "$source_ami" 2>/dev/null; then
|
|
is_orphan="true"
|
|
fi
|
|
|
|
if [[ "$is_orphan" == "true" ]]; then
|
|
((orphan_count++)) || true
|
|
local size_gb="${snap_size:-0}"
|
|
((total_size += size_gb)) || true
|
|
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
log_info "[DRY RUN] Would delete orphan snapshot ${snap_id} (${size_gb} GiB, source AMI: ${source_ami:-unknown})"
|
|
else
|
|
log_info "Deleting orphan snapshot ${snap_id} (${size_gb} GiB)..."
|
|
if aws_cmd ec2 delete-snapshot --snapshot-id "$snap_id" >/dev/null 2>&1; then
|
|
((deleted_count++)) || true
|
|
else
|
|
log_warn "Failed to delete snapshot ${snap_id}"
|
|
fi
|
|
fi
|
|
fi
|
|
done < <(echo "$snaps_json" | jq -r '.[] | [.SnapshotId, (.Tags // [] | map(select(.Key == "source-ami")) | .[0].Value // ""), (.VolumeSize // 0 | tostring)] | @tsv')
|
|
|
|
echo ""
|
|
echo "Summary"
|
|
printf " Total managed snapshots: %d\n" "$total_snaps"
|
|
printf " Orphaned: %d\n" "$orphan_count"
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
printf " Would delete: %d\n" "$orphan_count"
|
|
else
|
|
printf " Deleted: %d\n" "$deleted_count"
|
|
fi
|
|
printf " Storage reclaimed: %d GiB\n" "$total_size"
|
|
}
|
|
|
|
# ── Inventory report ──────────────────────────────────────────────────
|
|
inventory_report() {
|
|
log_info "Generating AMI inventory report..."
|
|
|
|
local amis_json
|
|
amis_json=$(get_managed_amis)
|
|
|
|
local total
|
|
total=$(echo "$amis_json" | jq 'length')
|
|
log_info "Found ${total} managed AMI(s)"
|
|
|
|
if (( total == 0 )); then
|
|
log_info "No managed AMIs found"
|
|
return
|
|
fi
|
|
|
|
local account_id
|
|
account_id=$(aws_cmd sts get-caller-identity --query Account --output text 2>/dev/null)
|
|
|
|
case "$OUTPUT_FORMAT" in
|
|
text)
|
|
echo ""
|
|
printf " %-24s %-30s %-12s %-6s %-11s %s\n" \
|
|
"AMI" "SOURCE INSTANCE" "CREATED" "AGE" "RETENTION" "SNAPSHOTS"
|
|
echo " ────────────────────────────────────────────────────────────────────────────────────────────────"
|
|
;;
|
|
csv)
|
|
echo "ami_id,name,source_instance,source_name,created_date,age_days,retention_days,expires,snapshot_count"
|
|
;;
|
|
esac
|
|
|
|
local json_items=()
|
|
|
|
while IFS=$'\t' read -r ami_id ami_name source_instance source_name created_date retention_tag expires_tag snap_count; do
|
|
[[ -z "$ami_id" || "$ami_id" == "null" ]] && continue
|
|
|
|
# Defaults for missing tags
|
|
[[ "$source_instance" == "null" || -z "$source_instance" ]] && source_instance="N/A"
|
|
[[ "$source_name" == "null" || -z "$source_name" ]] && source_name=""
|
|
[[ "$created_date" == "null" || -z "$created_date" ]] && created_date="unknown"
|
|
[[ "$retention_tag" == "null" || -z "$retention_tag" ]] && retention_tag="$RETENTION_DAYS"
|
|
[[ "$expires_tag" == "null" || -z "$expires_tag" ]] && expires_tag="N/A"
|
|
[[ "$snap_count" == "null" || -z "$snap_count" ]] && snap_count="0"
|
|
|
|
local age=0
|
|
if [[ "$created_date" != "unknown" ]]; then
|
|
age=$(days_since "$created_date")
|
|
fi
|
|
|
|
local instance_display="$source_instance"
|
|
if [[ -n "$source_name" && "$source_name" != "N/A" ]]; then
|
|
instance_display="${source_instance} (${source_name})"
|
|
fi
|
|
|
|
case "$OUTPUT_FORMAT" in
|
|
text)
|
|
printf " %-24s %-30s %-12s %3dd %3dd %s\n" \
|
|
"$ami_id" "$instance_display" "$created_date" "$age" "$retention_tag" "$snap_count"
|
|
;;
|
|
csv)
|
|
echo "\"${ami_id}\",\"${ami_name}\",\"${source_instance}\",\"${source_name}\",\"${created_date}\",${age},${retention_tag},\"${expires_tag}\",${snap_count}"
|
|
;;
|
|
json)
|
|
json_items+=("{\"ami_id\":\"${ami_id}\",\"name\":\"${ami_name}\",\"source_instance\":\"${source_instance}\",\"source_name\":\"${source_name}\",\"created_date\":\"${created_date}\",\"age_days\":${age},\"retention_days\":${retention_tag},\"expires\":\"${expires_tag}\",\"snapshot_count\":${snap_count}}")
|
|
;;
|
|
esac
|
|
done < <(echo "$amis_json" | jq -r '.[] | [
|
|
.ImageId,
|
|
(.Tags // [] | map(select(.Key == "Name")) | .[0].Value // "N/A"),
|
|
(.Tags // [] | map(select(.Key == "source-instance")) | .[0].Value // ""),
|
|
(.Tags // [] | map(select(.Key == "source-name")) | .[0].Value // ""),
|
|
(.Tags // [] | map(select(.Key == "created-date")) | .[0].Value // ""),
|
|
(.Tags // [] | map(select(.Key == "retention-days")) | .[0].Value // ""),
|
|
(.Tags // [] | map(select(.Key == "expires")) | .[0].Value // ""),
|
|
(.BlockDeviceMappings // [] | map(select(.Ebs.SnapshotId)) | length | tostring)
|
|
] | @tsv')
|
|
|
|
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
|
|
local joined
|
|
joined=$(printf ",%s" "${json_items[@]}")
|
|
joined="${joined:1}"
|
|
printf '{"mode":"inventory","total":%d,"items":[%s]}\n' "$total" "$joined"
|
|
fi
|
|
|
|
if [[ "$OUTPUT_FORMAT" == "text" ]]; then
|
|
echo ""
|
|
printf " Total: %d managed AMI(s)\n" "$total"
|
|
fi
|
|
}
|
|
|
|
# ── Main ──────────────────────────────────────────────────────────────
|
|
main() {
|
|
parse_args "$@"
|
|
setup_colors
|
|
check_deps
|
|
|
|
START_TIME=$(date +%s)
|
|
|
|
print_header
|
|
|
|
case "$RUN_MODE" in
|
|
create)
|
|
create_ami
|
|
;;
|
|
enforce)
|
|
enforce_retention
|
|
;;
|
|
clean-snapshots)
|
|
clean_orphan_snapshots
|
|
;;
|
|
inventory)
|
|
inventory_report
|
|
;;
|
|
*)
|
|
die "Unknown mode: $RUN_MODE"
|
|
;;
|
|
esac
|
|
|
|
local elapsed=$(( $(date +%s) - START_TIME ))
|
|
log_info "Completed in ${elapsed}s"
|
|
}
|
|
|
|
main "$@"
|