Files
linux-scripts/contabo-snapshot-manager.sh
T
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
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.
2026-05-25 03:31:08 +02:00

707 lines
27 KiB
Bash

#!/usr/bin/env bash
#########################################################################################
#### contabo-snapshot-manager.sh — Create, rotate, list, audit, and restore Contabo ####
#### VPS/VDS snapshots via the REST API. Automated retention and fleet-wide ops ####
#### Requires: bash 4+, curl, jq ####
#### ####
#### Author: Phil Connor ####
#### Contact: contact@mylinux.work ####
#### License: MIT ####
#### Version 1.01 ####
#### ####
#### Usage: ####
#### ./contabo-snapshot-manager.sh --snapshot --all ####
#### ####
#### See --help for all options. ####
#########################################################################################
set -euo pipefail
# ── Colors (pre-initialized) ─────────────────────────────────────────
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" DIM="" RESET=""
setup_colors() {
if [[ "${COLOR:-auto}" == "never" ]]; then
return
fi
if [[ "${COLOR:-auto}" == "always" ]] || [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'
fi
}
# ── Logging ───────────────────────────────────────────────────────────
log() { echo -e "${BLUE}[INFO]${RESET} $*"; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${DIM}[DEBUG]${RESET} $*"; fi; }
die() { err "$*"; exit 1; }
section_header() {
echo ""
echo -e " ${BOLD}${CYAN}── $1 ──${RESET}"
echo ""
}
field() {
printf " ${BOLD}%-22s${RESET} %s\n" "$1" "$2"
}
field_color() {
printf " ${BOLD}%-22s${RESET} %b\n" "$1" "$2"
}
elapsed() {
local end_time
end_time=$(date +%s)
echo "$(( end_time - START_TIME ))s"
}
# ── Defaults ──────────────────────────────────────────────────────────
RUN_MODE=""
ALSO_ROTATE="false"
INSTANCE_ID=""
TARGET_ALL="false"
SNAPSHOT_ID=""
KEEP="${CSM_KEEP:-3}"
PREFIX="${CSM_PREFIX:-auto}"
MAX_AGE="${CSM_MAX_AGE:-7}"
OUTPUT_FORMAT="${CSM_FORMAT:-text}"
DRY_RUN="true"
FORCE="false"
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"
# ── Credentials ───────────────────────────────────────────────────────
CONTABO_CLIENT_ID="${CONTABO_CLIENT_ID:-}"
CONTABO_CLIENT_SECRET="${CONTABO_CLIENT_SECRET:-}"
CONTABO_API_USER="${CONTABO_API_USER:-}"
CONTABO_API_PASS="${CONTABO_API_PASS:-}"
# ── State ─────────────────────────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
START_TIME=""
SNAP_CREATED=0
SNAP_DELETED=0
SNAP_ERRORS=0
# ── API helpers ──────────────────────────────────────────────────────
contabo_token() {
local resp
resp=$(curl -s -d "client_id=${CONTABO_CLIENT_ID}" \
-d "client_secret=${CONTABO_CLIENT_SECRET}" \
--data-urlencode "username=${CONTABO_API_USER}" \
--data-urlencode "password=${CONTABO_API_PASS}" \
-d "grant_type=password" \
"https://auth.contabo.com/auth/realms/contabo/protocol/openid-connect/token")
local token
token=$(echo "$resp" | jq -r '.access_token // empty' 2>/dev/null)
if [[ -z "$token" ]]; then
die "Failed to obtain access token — check credentials"
fi
echo "$token"
}
contabo_api() {
local method="$1" endpoint="$2"
shift 2
local attempt=0 max_attempts=3
while (( attempt < max_attempts )); do
local http_code
http_code=$(curl -s -o /tmp/csm_resp.json -w "%{http_code}" \
-X "$method" \
-H "Authorization: Bearer $(contabo_token)" \
-H "Content-Type: application/json" \
-H "x-request-id: $(cat /proc/sys/kernel/random/uuid 2>/dev/null || date +%s%N)" \
"https://api.contabo.com/v1${endpoint}" "$@")
verbose "API ${method} ${endpoint} → HTTP ${http_code}"
if [[ "$http_code" == "429" ]]; then
((attempt++)) || true
local wait=$(( attempt * 5 ))
warn "Rate limited — retrying in ${wait}s (attempt ${attempt}/${max_attempts})"
sleep "$wait"
continue
fi
cat /tmp/csm_resp.json
return 0
done
err "API request failed after ${max_attempts} attempts: ${method} ${endpoint}"
return 1
}
check_credentials() {
[[ -z "$CONTABO_CLIENT_ID" ]] && die "CONTABO_CLIENT_ID not set"
[[ -z "$CONTABO_CLIENT_SECRET" ]] && die "CONTABO_CLIENT_SECRET not set"
[[ -z "$CONTABO_API_USER" ]] && die "CONTABO_API_USER not set"
[[ -z "$CONTABO_API_PASS" ]] && die "CONTABO_API_PASS not set"
}
check_deps() {
command -v curl &>/dev/null || die "curl is required"
command -v jq &>/dev/null || die "jq is required"
}
# ── Instance helpers ─────────────────────────────────────────────────
get_all_instance_ids() {
local page=1 size=100 ids=""
while true; do
local resp
resp=$(contabo_api GET "/compute/instances?page=${page}&size=${size}")
local page_ids
page_ids=$(echo "$resp" | jq -r '.data[].instanceId' 2>/dev/null)
[[ -z "$page_ids" ]] && break
ids="${ids}${ids:+$'\n'}${page_ids}"
local count
count=$(echo "$page_ids" | wc -l)
(( count < size )) && break
((page++)) || true
done
echo "$ids"
}
get_instance_name() {
local iid="$1"
contabo_api GET "/compute/instances/${iid}" \
| jq -r '.data[0].name // .data[0].displayName // "unknown"' 2>/dev/null
}
get_instance_ids() {
if [[ "$TARGET_ALL" == "true" ]]; then
get_all_instance_ids
elif [[ -n "$INSTANCE_ID" ]]; then
echo "$INSTANCE_ID"
else
die "Specify --instance ID or --all"
fi
}
# ── Snapshot helpers ─────────────────────────────────────────────────
get_snapshots() {
local iid="$1"
contabo_api GET "/compute/instances/${iid}/snapshots" \
| jq -r '.data // []' 2>/dev/null
}
# ══════════════════════════════════════════════════════════════════════
# SNAPSHOT
# ══════════════════════════════════════════════════════════════════════
do_snapshot() {
local ids
ids=$(get_instance_ids)
[[ -z "$ids" ]] && die "No instances found"
local count
count=$(echo "$ids" | grep -c . || true)
local target_label="instance ${INSTANCE_ID}"
[[ "$TARGET_ALL" == "true" ]] && target_label="all (${count} instances)"
section_header "Creating Snapshots"
field "Target:" "$target_label"
field "Prefix:" "$PREFIX"
echo ""
while IFS= read -r iid; do
[[ -z "$iid" ]] && continue
local snap_name
snap_name="${PREFIX}-$(date +%Y%m%d-%H%M%S)"
local iname
iname=$(get_instance_name "$iid")
verbose "Snapshotting ${iname} (${iid}) as ${snap_name}"
if contabo_api POST "/compute/instances/${iid}/snapshots" \
-d "{\"name\": \"${snap_name}\", \"description\": \"Managed by ${SCRIPT_NAME}\"}" > /dev/null 2>&1; then
echo -e " ${GREEN}${RESET} ${iname} (${iid}) ${snap_name}"
((SNAP_CREATED++)) || true
else
echo -e " ${RED}${RESET} ${iname} (${iid}) failed"
((SNAP_ERRORS++)) || true
fi
# Brief pause to avoid rate limiting on large fleets
sleep 1
done <<< "$ids"
echo ""
field_color "Created:" "${GREEN}${SNAP_CREATED}${RESET}"
if [[ "$SNAP_ERRORS" -gt 0 ]]; then
field_color "Errors:" "${RED}${SNAP_ERRORS}${RESET}"
fi
if [[ "$ALSO_ROTATE" == "true" ]]; then
do_rotate
fi
}
# ══════════════════════════════════════════════════════════════════════
# ROTATE
# ══════════════════════════════════════════════════════════════════════
do_rotate() {
local ids
ids=$(get_instance_ids)
[[ -z "$ids" ]] && die "No instances found"
section_header "Rotating Snapshots"
field "Keep:" "$KEEP per instance"
field "Prefix:" "$PREFIX"
if [[ "$DRY_RUN" == "true" && "$FORCE" != "true" ]]; then
field_color "Mode:" "${YELLOW}DRY-RUN${RESET} (use --force to delete)"
else
field_color "Mode:" "${RED}LIVE${RESET}"
fi
echo ""
while IFS= read -r iid; do
[[ -z "$iid" ]] && continue
local iname
iname=$(get_instance_name "$iid")
local snaps
snaps=$(get_snapshots "$iid")
# Filter to managed snapshots (matching prefix), sort by date descending
local managed
managed=$(echo "$snaps" | jq -r \
--arg prefix "$PREFIX" \
'[.[] | select(.name | startswith($prefix))] | sort_by(.createdDate) | reverse' \
2>/dev/null)
local total
total=$(echo "$managed" | jq 'length' 2>/dev/null || echo 0)
if (( total <= KEEP )); then
verbose "${iname}: ${total} managed snapshots, keeping all"
continue
fi
local to_delete
to_delete=$(echo "$managed" | jq -r ".[$KEEP:][] | .snapshotId" 2>/dev/null)
while IFS= read -r sid; do
[[ -z "$sid" ]] && continue
if [[ "$DRY_RUN" == "true" && "$FORCE" != "true" ]]; then
echo -e " ${YELLOW}${RESET} Would delete: ${iname} (${iid}) → ${sid}"
else
if contabo_api DELETE "/compute/instances/${iid}/snapshots/${sid}" > /dev/null 2>&1; then
echo -e " ${GREEN}${RESET} Deleted: ${iname} (${iid}) → ${sid}"
((SNAP_DELETED++)) || true
else
echo -e " ${RED}${RESET} Failed: ${iname} (${iid}) → ${sid}"
((SNAP_ERRORS++)) || true
fi
sleep 1
fi
done <<< "$to_delete"
done <<< "$ids"
echo ""
if [[ "$DRY_RUN" == "true" && "$FORCE" != "true" ]]; then
log "Dry-run complete — use --force to execute"
else
field_color "Deleted:" "${GREEN}${SNAP_DELETED}${RESET}"
if [[ "$SNAP_ERRORS" -gt 0 ]]; then
field_color "Errors:" "${RED}${SNAP_ERRORS}${RESET}"
fi
fi
}
# ══════════════════════════════════════════════════════════════════════
# LIST
# ══════════════════════════════════════════════════════════════════════
do_list() {
local ids
ids=$(get_instance_ids)
[[ -z "$ids" ]] && die "No instances found"
section_header "Snapshots"
printf " ${BOLD}%-8s %-18s %-28s %-22s${RESET}\n" "INST" "SNAPSHOT ID" "NAME" "CREATED"
printf " %s\n" "$(printf '%.0s─' {1..78})"
local total=0
while IFS= read -r iid; do
[[ -z "$iid" ]] && continue
local snaps
snaps=$(get_snapshots "$iid")
echo "$snaps" | jq -r --arg iid "$iid" \
'.[] | "\($iid)\t\(.snapshotId)\t\(.name)\t\(.createdDate)"' 2>/dev/null \
| while IFS=$'\t' read -r inst sid name created; do
printf " %-8s %-18s %-28s %-22s\n" "$inst" "$sid" "${name:0:26}" "${created:0:20}"
((total++)) 2>/dev/null || true
done
done <<< "$ids"
}
# ══════════════════════════════════════════════════════════════════════
# AUDIT
# ══════════════════════════════════════════════════════════════════════
do_audit() {
local ids
ids=$(get_all_instance_ids)
[[ -z "$ids" ]] && die "No instances found"
section_header "Snapshot Audit"
printf " ${BOLD}%-20s %-20s %6s %6s %-12s${RESET}\n" \
"INSTANCE" "LATEST SNAPSHOT" "AGE" "COUNT" "STATUS"
printf " %s\n" "$(printf '%.0s─' {1..68})"
local protected=0 stale=0 unprotected=0 total_instances=0
local now
now=$(date +%s)
while IFS= read -r iid; do
[[ -z "$iid" ]] && continue
((total_instances++)) || true
local iname
iname=$(get_instance_name "$iid")
local snaps
snaps=$(get_snapshots "$iid")
local snap_count
snap_count=$(echo "$snaps" | jq 'length' 2>/dev/null || echo 0)
if [[ "$snap_count" -eq 0 ]]; then
printf " %-20s %-20s %6s %6s " "${iname:0:18}" "(none)" "—" "0"
echo -e "${RED}✗ Unprotected${RESET}"
((unprotected++)) || true
continue
fi
local latest
latest=$(echo "$snaps" | jq -r \
'[.[] | select(.name)] | sort_by(.createdDate) | last' 2>/dev/null)
local latest_name latest_date
latest_name=$(echo "$latest" | jq -r '.name // "unknown"' 2>/dev/null)
latest_date=$(echo "$latest" | jq -r '.createdDate // ""' 2>/dev/null)
local age_days="?"
if [[ -n "$latest_date" ]]; then
local snap_epoch
snap_epoch=$(date -d "$latest_date" +%s 2>/dev/null || echo 0)
if [[ "$snap_epoch" -gt 0 ]]; then
age_days=$(( (now - snap_epoch) / 86400 ))
fi
fi
local status_str status_color
if [[ "$age_days" != "?" ]] && (( age_days > MAX_AGE )); then
status_str="⚠ Stale"
status_color="$YELLOW"
((stale++)) || true
else
status_str="✓ OK"
status_color="$GREEN"
((protected++)) || true
fi
printf " %-20s %-20s %5sd %6s " \
"${iname:0:18}" "${latest_name:0:18}" "$age_days" "$snap_count"
echo -e "${status_color}${status_str}${RESET}"
done <<< "$ids"
echo ""
field "Instances:" "$total_instances"
field_color "Protected:" "${GREEN}${protected}${RESET}"
if [[ "$stale" -gt 0 ]]; then
field_color "Stale (>${MAX_AGE}d):" "${YELLOW}${stale}${RESET}"
fi
if [[ "$unprotected" -gt 0 ]]; then
field_color "Unprotected:" "${RED}${unprotected}${RESET}"
fi
}
# ══════════════════════════════════════════════════════════════════════
# RESTORE
# ══════════════════════════════════════════════════════════════════════
do_restore() {
[[ -z "$INSTANCE_ID" ]] && die "Specify --instance ID"
[[ -z "$SNAPSHOT_ID" ]] && die "Specify --snapshot-id ID"
local iname
iname=$(get_instance_name "$INSTANCE_ID")
section_header "Restore Snapshot"
field "Instance:" "${iname} (${INSTANCE_ID})"
field "Snapshot:" "$SNAPSHOT_ID"
echo ""
if [[ "$FORCE" != "true" ]]; then
echo -e " ${RED}WARNING: This will revert the instance to the snapshot state.${RESET}"
echo -e " ${RED}All changes since the snapshot will be lost.${RESET}"
echo ""
read -r -p " Type 'yes' to confirm: " confirm
if [[ "$confirm" != "yes" ]]; then
log "Restore cancelled"
return
fi
fi
if contabo_api POST "/compute/instances/${INSTANCE_ID}/snapshots/${SNAPSHOT_ID}" \
-d '{}' > /dev/null 2>&1; then
echo -e " ${GREEN}${RESET} Restore initiated — instance will revert to ${SNAPSHOT_ID}"
log "Monitor instance status — revert may take several minutes"
else
echo -e " ${RED}${RESET} Restore failed"
fi
}
# ══════════════════════════════════════════════════════════════════════
# STATUS
# ══════════════════════════════════════════════════════════════════════
do_status() {
local ids
ids=$(get_all_instance_ids)
[[ -z "$ids" ]] && die "No instances found"
local total_instances=0 total_snaps=0
local protected=0 stale=0 unprotected=0
local now
now=$(date +%s)
while IFS= read -r iid; do
[[ -z "$iid" ]] && continue
((total_instances++)) || true
local snaps
snaps=$(get_snapshots "$iid")
local snap_count
snap_count=$(echo "$snaps" | jq 'length' 2>/dev/null || echo 0)
total_snaps=$(( total_snaps + snap_count ))
if [[ "$snap_count" -eq 0 ]]; then
((unprotected++)) || true
continue
fi
local latest_date
latest_date=$(echo "$snaps" | jq -r \
'[.[] | select(.createdDate)] | sort_by(.createdDate) | last | .createdDate // ""' \
2>/dev/null)
if [[ -n "$latest_date" ]]; then
local snap_epoch
snap_epoch=$(date -d "$latest_date" +%s 2>/dev/null || echo 0)
if [[ "$snap_epoch" -gt 0 ]]; then
local age_days=$(( (now - snap_epoch) / 86400 ))
if (( age_days > MAX_AGE )); then
((stale++)) || true
else
((protected++)) || true
fi
else
((protected++)) || true
fi
else
((protected++)) || true
fi
done <<< "$ids"
if [[ "$OUTPUT_FORMAT" == "prometheus" ]]; then
cat <<EOF
# HELP contabo_snapshot_instances_total Total Contabo instances
# TYPE contabo_snapshot_instances_total gauge
contabo_snapshot_instances_total ${total_instances}
# HELP contabo_snapshot_total Total snapshots across all instances
# TYPE contabo_snapshot_total gauge
contabo_snapshot_total ${total_snaps}
# HELP contabo_snapshot_protected_instances Instances with recent snapshots
# TYPE contabo_snapshot_protected_instances gauge
contabo_snapshot_protected_instances ${protected}
# HELP contabo_snapshot_stale_instances Instances with snapshots older than threshold
# TYPE contabo_snapshot_stale_instances gauge
contabo_snapshot_stale_instances ${stale}
# HELP contabo_snapshot_unprotected_instances Instances with no snapshots
# TYPE contabo_snapshot_unprotected_instances gauge
contabo_snapshot_unprotected_instances ${unprotected}
# HELP contabo_snapshot_max_age_days Stale threshold in days
# TYPE contabo_snapshot_max_age_days gauge
contabo_snapshot_max_age_days ${MAX_AGE}
# HELP contabo_snapshot_retention_count Retention count setting
# TYPE contabo_snapshot_retention_count gauge
contabo_snapshot_retention_count ${KEEP}
EOF
return
fi
section_header "Fleet Snapshot Status"
field "Instances:" "$total_instances"
field "Total snapshots:" "$total_snaps"
field_color "Protected:" "${GREEN}${protected}${RESET}"
if [[ "$stale" -gt 0 ]]; then
field_color "Stale (>${MAX_AGE}d):" "${YELLOW}${stale}${RESET}"
else
field_color "Stale (>${MAX_AGE}d):" "${GREEN}0${RESET}"
fi
if [[ "$unprotected" -gt 0 ]]; then
field_color "Unprotected:" "${RED}${unprotected}${RESET}"
else
field_color "Unprotected:" "${GREEN}0${RESET}"
fi
}
# ══════════════════════════════════════════════════════════════════════
# HELP
# ══════════════════════════════════════════════════════════════════════
show_help() {
cat <<EOF
${BOLD}${SCRIPT_NAME}${RESET} — Contabo Snapshot Manager
Create, rotate, list, audit, and restore Contabo VPS/VDS snapshots
via the REST API.
${BOLD}MODES${RESET}
--snapshot Create snapshots
--rotate Prune old managed snapshots
--list List all snapshots
--audit Check snapshot coverage and staleness
--restore Revert an instance to a snapshot
--status Fleet-wide snapshot summary
${BOLD}TARGETING${RESET}
--instance ID Target a specific instance
--all Target all instances
${BOLD}OPTIONS${RESET}
--keep N Snapshots to retain per instance (default: 3)
--prefix STR Managed snapshot name prefix (default: auto)
--max-age DAYS Stale threshold for audit (default: 7)
--snapshot-id ID Snapshot ID for restore
--format FMT Output: text, prometheus (default: text)
--dry-run Preview deletions without executing (default)
--force Execute deletions / skip confirmations
--verbose Debug output
--no-color Disable colored output
--help Show this help message
${BOLD}ENVIRONMENT VARIABLES${RESET}
CONTABO_CLIENT_ID OAuth2 Client ID (required)
CONTABO_CLIENT_SECRET OAuth2 Client Secret (required)
CONTABO_API_USER API username / email (required)
CONTABO_API_PASS API password (required)
CSM_KEEP Default retention count
CSM_PREFIX Default snapshot prefix
CSM_MAX_AGE Default stale threshold (days)
CSM_FORMAT Default output format
VERBOSE Enable verbose output (true/false)
COLOR Color mode: auto, always, never
${BOLD}EXAMPLES${RESET}
# Snapshot a single instance
${SCRIPT_NAME} --snapshot --instance 12345
# Snapshot all instances
${SCRIPT_NAME} --snapshot --all
# Snapshot and rotate in one run
${SCRIPT_NAME} --snapshot --rotate --all --keep 3 --force
# Dry-run rotation
${SCRIPT_NAME} --rotate --all --keep 3
# Execute rotation
${SCRIPT_NAME} --rotate --all --keep 3 --force
# List all snapshots
${SCRIPT_NAME} --list --all
# Audit fleet snapshot health
${SCRIPT_NAME} --audit
# Restore an instance
${SCRIPT_NAME} --restore --instance 12345 --snapshot-id snap1628603855
# Prometheus metrics output
${SCRIPT_NAME} --status --format prometheus
${BOLD}EXIT CODES${RESET}
0 Success
1 Runtime error
EOF
}
# ══════════════════════════════════════════════════════════════════════
# PARSE ARGS
# ══════════════════════════════════════════════════════════════════════
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--snapshot) RUN_MODE="snapshot"; shift ;;
--rotate)
if [[ "$RUN_MODE" == "snapshot" ]]; then
ALSO_ROTATE="true"
else
RUN_MODE="rotate"
fi
shift ;;
--list) RUN_MODE="list"; shift ;;
--audit) RUN_MODE="audit"; shift ;;
--restore) RUN_MODE="restore"; shift ;;
--status) RUN_MODE="status"; shift ;;
--instance) INSTANCE_ID="${2:?--instance requires an ID}"; shift 2 ;;
--all) TARGET_ALL="true"; shift ;;
--keep) KEEP="${2:?--keep requires a number}"; shift 2 ;;
--prefix) PREFIX="${2:?--prefix requires a string}"; shift 2 ;;
--max-age) MAX_AGE="${2:?--max-age requires days}"; shift 2 ;;
--snapshot-id) SNAPSHOT_ID="${2:?--snapshot-id requires an ID}"; shift 2 ;;
--format) OUTPUT_FORMAT="${2:?--format requires a value}"; shift 2 ;;
--dry-run) DRY_RUN="true"; shift ;;
--force) FORCE="true"; shift ;;
--verbose) VERBOSE="true"; shift ;;
--no-color) COLOR="never"; shift ;;
--help|-h) setup_colors; show_help; exit 0 ;;
*) die "Unknown option: $1 (see --help)" ;;
esac
done
}
# ══════════════════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════════════════
main() {
parse_args "$@"
setup_colors
if [[ -z "$RUN_MODE" ]]; then
err "No mode specified"
echo ""
show_help
exit 1
fi
check_deps
check_credentials
START_TIME=$(date +%s)
case "$RUN_MODE" in
snapshot) do_snapshot ;;
rotate) do_rotate ;;
list) do_list ;;
audit) do_audit ;;
restore) do_restore ;;
status) do_status ;;
*) die "Unknown mode: ${RUN_MODE}" ;;
esac
if [[ "$OUTPUT_FORMAT" != "prometheus" ]]; then
echo ""
field "Duration:" "$(elapsed)"
fi
}
main "$@"