466 lines
14 KiB
Bash
Executable File
466 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set +x
|
|
set -euo pipefail
|
|
|
|
SCRIPT_NAME="$(basename "$0")"
|
|
|
|
# Source tenancy/profile. Source images are produced in desktopasaservicedev.
|
|
SOURCE_PROFILE="${SOURCE_PROFILE:-MCP_GW_DEFAULT}"
|
|
SOURCE_AUTH="${SOURCE_AUTH:---auth security_token}"
|
|
read -r -a SOURCE_AUTH_ARGS <<< "$SOURCE_AUTH"
|
|
SOURCE_REGION="${SOURCE_REGION:-us-phoenix-1}"
|
|
SOURCE_EXPORT_BUCKET="${SOURCE_EXPORT_BUCKET:-desktop-images-OL-uploads}"
|
|
SOURCE_NAMESPACE="${SOURCE_NAMESPACE:-lrkymkoddq0d}"
|
|
|
|
# Target tenancy/profile.
|
|
TARGET_PROFILE="${TARGET_PROFILE:-daaspreproduction}"
|
|
TARGET_REGION="${TARGET_REGION:-us-phoenix-1}"
|
|
TARGET_COMPARTMENT_OCID="${TARGET_COMPARTMENT_OCID:-ocid1.compartment.oc1..aaaaaaaarkpwynhpyooftr24a7s7gfjdhkerzidkqyrcmqj7ahijti6pilkq}"
|
|
TARGET_NAMESPACE="${TARGET_NAMESPACE:-id2t37chxkmi}"
|
|
|
|
# Import/export behavior.
|
|
EXPORT_FORMAT="${EXPORT_FORMAT:-OCI}"
|
|
SOURCE_IMAGE_TYPE="${SOURCE_IMAGE_TYPE:-QCOW2}"
|
|
TARGET_DISPLAY_NAME_PREFIX="${TARGET_DISPLAY_NAME_PREFIX:-OSD }"
|
|
PAR_TTL_SECONDS="${PAR_TTL_SECONDS:-21600}"
|
|
EXPORT_POLL_INTERVAL_SECONDS="${EXPORT_POLL_INTERVAL_SECONDS:-60}"
|
|
EXPORT_TIMEOUT_SECONDS="${EXPORT_TIMEOUT_SECONDS:-21600}"
|
|
IMPORT_POLL_INTERVAL_SECONDS="${IMPORT_POLL_INTERVAL_SECONDS:-60}"
|
|
IMPORT_TIMEOUT_SECONDS="${IMPORT_TIMEOUT_SECONDS:-21600}"
|
|
COPY_DEFINED_TAGS="${COPY_DEFINED_TAGS:-1}"
|
|
DELETE_SOURCE_EXPORT_OBJECT="${DELETE_SOURCE_EXPORT_OBJECT:-0}"
|
|
PREFLIGHT_ONLY="${PREFLIGHT_ONLY:-0}"
|
|
|
|
PAR_ID=""
|
|
PAR_CREATED=0
|
|
TEMP_DIR=""
|
|
EXPORT_WORK_REQUEST_ID=""
|
|
EXPORT_OBJECT_NAME=""
|
|
|
|
usage() {
|
|
cat >&2 <<EOF
|
|
Usage: ${SCRIPT_NAME} SOURCE_IMAGE_OCID
|
|
|
|
Copies one custom image from desktopasaservicedev to daaspreproduction.
|
|
|
|
Tunable constants can be overridden with environment variables:
|
|
SOURCE_PROFILE SOURCE_AUTH SOURCE_REGION SOURCE_EXPORT_BUCKET SOURCE_NAMESPACE
|
|
TARGET_PROFILE TARGET_REGION TARGET_COMPARTMENT_OCID TARGET_NAMESPACE
|
|
TARGET_DISPLAY_NAME_PREFIX COPY_DEFINED_TAGS DELETE_SOURCE_EXPORT_OBJECT
|
|
PREFLIGHT_ONLY=1 validates source and target access without exporting/importing.
|
|
EOF
|
|
}
|
|
|
|
log() {
|
|
printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >&2
|
|
}
|
|
|
|
fail() {
|
|
log "ERROR: $*"
|
|
exit 1
|
|
}
|
|
|
|
redact_stderr() {
|
|
sed -E \
|
|
-e 's#https://objectstorage\.[^[:space:]"'"'"']+#<redacted-objectstorage-uri>#g' \
|
|
-e 's#/p/[-A-Za-z0-9_./?=%&:+]+#<redacted-par-access-uri>#g'
|
|
}
|
|
|
|
oci_source() {
|
|
if [[ -n "$SOURCE_AUTH" ]]; then
|
|
oci "$@" \
|
|
--profile "$SOURCE_PROFILE" \
|
|
"${SOURCE_AUTH_ARGS[@]}" \
|
|
--region "$SOURCE_REGION"
|
|
else
|
|
oci "$@" \
|
|
--profile "$SOURCE_PROFILE" \
|
|
--region "$SOURCE_REGION"
|
|
fi
|
|
}
|
|
|
|
oci_target() {
|
|
oci "$@" \
|
|
--profile "$TARGET_PROFILE" \
|
|
--region "$TARGET_REGION"
|
|
}
|
|
|
|
require_command() {
|
|
command -v "$1" >/dev/null 2>&1 || fail "required command not found: $1"
|
|
}
|
|
|
|
cleanup() {
|
|
local status=$?
|
|
|
|
if [[ "$PAR_CREATED" -eq 1 && -n "$PAR_ID" ]]; then
|
|
log "Deleting temporary source ObjectRead PAR"
|
|
if ! oci_source os preauth-request delete \
|
|
--namespace-name "$SOURCE_NAMESPACE" \
|
|
--bucket-name "$SOURCE_EXPORT_BUCKET" \
|
|
--par-id "$PAR_ID" \
|
|
--force >/dev/null 2> >(redact_stderr >&2); then
|
|
log "WARNING: failed to delete temporary PAR"
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then
|
|
rm -rf "$TEMP_DIR"
|
|
fi
|
|
|
|
exit "$status"
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
json_string_or_empty() {
|
|
local file="$1"
|
|
local jq_filter="$2"
|
|
jq -r "${jq_filter} // empty" "$file"
|
|
}
|
|
|
|
utc_after_seconds() {
|
|
local seconds="$1"
|
|
local epoch
|
|
epoch="$(($(date -u +%s) + seconds))"
|
|
|
|
if date -u -r "$epoch" +%Y-%m-%dT%H:%M:%SZ >/dev/null 2>&1; then
|
|
date -u -r "$epoch" +%Y-%m-%dT%H:%M:%SZ
|
|
else
|
|
date -u -d "@$epoch" +%Y-%m-%dT%H:%M:%SZ
|
|
fi
|
|
}
|
|
|
|
wait_for_export_object() {
|
|
local object_name="$1"
|
|
local start now elapsed
|
|
|
|
start="$(date -u +%s)"
|
|
while true; do
|
|
if oci_source os object head \
|
|
--namespace-name "$SOURCE_NAMESPACE" \
|
|
--bucket-name "$SOURCE_EXPORT_BUCKET" \
|
|
--name "$object_name" >/dev/null 2>/dev/null; then
|
|
log "Export object is available"
|
|
return 0
|
|
fi
|
|
|
|
now="$(date -u +%s)"
|
|
elapsed=$((now - start))
|
|
if (( elapsed >= EXPORT_TIMEOUT_SECONDS )); then
|
|
fail "timed out waiting for exported object after ${EXPORT_TIMEOUT_SECONDS}s"
|
|
fi
|
|
|
|
if [[ -n "$EXPORT_WORK_REQUEST_ID" ]]; then
|
|
log "Waiting for export object (${elapsed}s elapsed); export work request: $(export_work_request_summary "$EXPORT_WORK_REQUEST_ID")"
|
|
else
|
|
log "Waiting for export object (${elapsed}s elapsed)"
|
|
fi
|
|
sleep "$EXPORT_POLL_INTERVAL_SECONDS"
|
|
done
|
|
}
|
|
|
|
export_work_request_summary() {
|
|
local work_request_id="$1"
|
|
local summary
|
|
|
|
if ! summary="$(
|
|
oci_source work-requests work-request get \
|
|
--work-request-id "$work_request_id" \
|
|
--query 'data.{status:status,percentComplete:"percent-complete"}' \
|
|
--raw-output 2>/dev/null
|
|
)"; then
|
|
printf 'unknown'
|
|
return 0
|
|
fi
|
|
|
|
jq -r '"\(.status) \(.percentComplete // 0)%"' <<<"$summary"
|
|
}
|
|
|
|
find_latest_export_work_request() {
|
|
oci_source work-requests work-request list \
|
|
--compartment-id "$SOURCE_COMPARTMENT_OCID" \
|
|
--resource-id "$SOURCE_IMAGE_OCID" \
|
|
--all \
|
|
--query 'data' \
|
|
--output json 2>/dev/null |
|
|
jq -r '
|
|
map(select(."operation-type" == "ExportImage"))
|
|
| sort_by(."time-accepted")
|
|
| last
|
|
| .id // empty
|
|
'
|
|
}
|
|
|
|
preflight() {
|
|
local source_state bucket_name target_namespace target_compartment_state
|
|
|
|
source_state="$(json_string_or_empty "$SOURCE_IMAGE_JSON" '.data."lifecycle-state"')"
|
|
if [[ "$source_state" != "AVAILABLE" ]]; then
|
|
fail "source image must be AVAILABLE, found: ${source_state:-unknown}"
|
|
fi
|
|
|
|
log "Checking source export bucket ${SOURCE_NAMESPACE}/${SOURCE_EXPORT_BUCKET}"
|
|
bucket_name="$(
|
|
oci_source os bucket get \
|
|
--namespace-name "$SOURCE_NAMESPACE" \
|
|
--bucket-name "$SOURCE_EXPORT_BUCKET" \
|
|
--query 'data.name' \
|
|
--raw-output \
|
|
2> >(redact_stderr >&2)
|
|
)"
|
|
if [[ "$bucket_name" != "$SOURCE_EXPORT_BUCKET" ]]; then
|
|
fail "unexpected source bucket response: ${bucket_name:-empty}"
|
|
fi
|
|
|
|
log "Checking target namespace"
|
|
target_namespace="$(
|
|
oci_target os ns get \
|
|
--query data \
|
|
--raw-output \
|
|
2> >(redact_stderr >&2)
|
|
)"
|
|
if [[ "$target_namespace" != "$TARGET_NAMESPACE" ]]; then
|
|
fail "target namespace mismatch: expected $TARGET_NAMESPACE, got ${target_namespace:-empty}"
|
|
fi
|
|
|
|
log "Checking target compartment"
|
|
target_compartment_state="$(
|
|
oci_target iam compartment get \
|
|
--compartment-id "$TARGET_COMPARTMENT_OCID" \
|
|
--query 'data."lifecycle-state"' \
|
|
--raw-output \
|
|
2> >(redact_stderr >&2)
|
|
)"
|
|
if [[ "$target_compartment_state" != "ACTIVE" ]]; then
|
|
fail "target compartment must be ACTIVE, found: ${target_compartment_state:-unknown}"
|
|
fi
|
|
}
|
|
|
|
delete_source_export_object() {
|
|
if [[ "$DELETE_SOURCE_EXPORT_OBJECT" != "1" || -z "$EXPORT_OBJECT_NAME" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
log "Deleting temporary source export object"
|
|
if ! oci_source os object delete \
|
|
--namespace-name "$SOURCE_NAMESPACE" \
|
|
--bucket-name "$SOURCE_EXPORT_BUCKET" \
|
|
--object-name "$EXPORT_OBJECT_NAME" \
|
|
--force >/dev/null \
|
|
2> >(redact_stderr >&2); then
|
|
log "WARNING: failed to delete temporary source export object"
|
|
fi
|
|
}
|
|
|
|
build_import_json() {
|
|
local include_defined_tags="$1"
|
|
|
|
jq -n \
|
|
--arg compartment_id "$TARGET_COMPARTMENT_OCID" \
|
|
--arg display_name "$TARGET_IMAGE_NAME" \
|
|
--arg source_image_type "$SOURCE_IMAGE_TYPE" \
|
|
--arg operating_system "$SOURCE_OPERATING_SYSTEM" \
|
|
--arg operating_system_version "$SOURCE_OPERATING_SYSTEM_VERSION" \
|
|
--arg launch_mode "$SOURCE_LAUNCH_MODE" \
|
|
--slurpfile freeform_tags "$TEMP_DIR/freeform-tags.json" \
|
|
--slurpfile defined_tags "$TEMP_DIR/defined-tags.json" \
|
|
--arg include_defined_tags "$include_defined_tags" \
|
|
'
|
|
{
|
|
compartmentId: $compartment_id,
|
|
displayName: $display_name,
|
|
uri: env.OCI_IMAGE_IMPORT_URI,
|
|
sourceImageType: $source_image_type
|
|
}
|
|
+ (if $operating_system == "" then {} else {operatingSystem: $operating_system} end)
|
|
+ (if $operating_system_version == "" then {} else {operatingSystemVersion: $operating_system_version} end)
|
|
+ (if $launch_mode == "" then {} else {launchMode: $launch_mode} end)
|
|
+ (if ($freeform_tags[0] // {}) == {} then {} else {freeformTags: $freeform_tags[0]} end)
|
|
+ (
|
|
if $include_defined_tags != "1" or ($defined_tags[0] // {}) == {} then
|
|
{}
|
|
else
|
|
{definedTags: $defined_tags[0]}
|
|
end
|
|
)
|
|
'
|
|
}
|
|
|
|
import_image() {
|
|
local include_defined_tags="$1"
|
|
|
|
export OCI_IMAGE_IMPORT_URI
|
|
oci_target compute image import from-object-uri \
|
|
--from-json "file://"<(build_import_json "$include_defined_tags") \
|
|
--query 'data.id' \
|
|
--raw-output \
|
|
2> >(redact_stderr >&2)
|
|
}
|
|
|
|
poll_target_image() {
|
|
local target_image_ocid="$1"
|
|
local start now elapsed state
|
|
|
|
start="$(date -u +%s)"
|
|
while true; do
|
|
state="$(
|
|
oci_target compute image get \
|
|
--image-id "$target_image_ocid" \
|
|
--query 'data."lifecycle-state"' \
|
|
--raw-output \
|
|
2> >(redact_stderr >&2)
|
|
)"
|
|
|
|
printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$state"
|
|
|
|
case "$state" in
|
|
AVAILABLE)
|
|
return 0
|
|
;;
|
|
FAILED)
|
|
fail "target image reached FAILED: $target_image_ocid"
|
|
;;
|
|
esac
|
|
|
|
now="$(date -u +%s)"
|
|
elapsed=$((now - start))
|
|
if (( elapsed >= IMPORT_TIMEOUT_SECONDS )); then
|
|
fail "timed out waiting for target image after ${IMPORT_TIMEOUT_SECONDS}s: $target_image_ocid"
|
|
fi
|
|
|
|
sleep "$IMPORT_POLL_INTERVAL_SECONDS"
|
|
done
|
|
}
|
|
|
|
if [[ "$#" -ne 1 ]]; then
|
|
usage
|
|
exit 2
|
|
fi
|
|
|
|
SOURCE_IMAGE_OCID="$1"
|
|
|
|
require_command oci
|
|
require_command jq
|
|
require_command sed
|
|
|
|
TEMP_DIR="$(mktemp -d)"
|
|
|
|
if [[ -z "$SOURCE_NAMESPACE" ]]; then
|
|
log "Resolving source Object Storage namespace"
|
|
SOURCE_NAMESPACE="$(
|
|
oci_source os ns get \
|
|
--query data \
|
|
--raw-output \
|
|
2> >(redact_stderr >&2)
|
|
)"
|
|
fi
|
|
|
|
log "Reading source image metadata"
|
|
SOURCE_IMAGE_JSON="$TEMP_DIR/source-image.json"
|
|
oci_source compute image get \
|
|
--image-id "$SOURCE_IMAGE_OCID" \
|
|
--output json \
|
|
>"$SOURCE_IMAGE_JSON" \
|
|
2> >(redact_stderr >&2)
|
|
|
|
SOURCE_DISPLAY_NAME="$(json_string_or_empty "$SOURCE_IMAGE_JSON" '.data."display-name"')"
|
|
SOURCE_OPERATING_SYSTEM="$(json_string_or_empty "$SOURCE_IMAGE_JSON" '.data."operating-system"')"
|
|
SOURCE_OPERATING_SYSTEM_VERSION="$(json_string_or_empty "$SOURCE_IMAGE_JSON" '.data."operating-system-version"')"
|
|
SOURCE_LAUNCH_MODE="$(json_string_or_empty "$SOURCE_IMAGE_JSON" '.data."launch-mode"')"
|
|
SOURCE_COMPARTMENT_OCID="$(json_string_or_empty "$SOURCE_IMAGE_JSON" '.data."compartment-id"')"
|
|
|
|
preflight
|
|
|
|
if [[ -n "$TARGET_DISPLAY_NAME_PREFIX" && "$SOURCE_DISPLAY_NAME" == "$TARGET_DISPLAY_NAME_PREFIX"* ]]; then
|
|
TARGET_IMAGE_NAME="$SOURCE_DISPLAY_NAME"
|
|
else
|
|
TARGET_IMAGE_NAME="${TARGET_DISPLAY_NAME_PREFIX}${SOURCE_DISPLAY_NAME}"
|
|
fi
|
|
|
|
if [[ -z "$TARGET_IMAGE_NAME" ]]; then
|
|
TARGET_IMAGE_NAME="Copied custom image ${SOURCE_IMAGE_OCID}"
|
|
fi
|
|
|
|
if [[ "$PREFLIGHT_ONLY" == "1" ]]; then
|
|
log "Preflight completed"
|
|
printf 'source_image=%s\n' "$SOURCE_IMAGE_OCID"
|
|
printf 'source_name=%s\n' "$SOURCE_DISPLAY_NAME"
|
|
printf 'target_name=%s\n' "$TARGET_IMAGE_NAME"
|
|
printf 'source_bucket=%s/%s\n' "$SOURCE_NAMESPACE" "$SOURCE_EXPORT_BUCKET"
|
|
printf 'target_compartment=%s\n' "$TARGET_COMPARTMENT_OCID"
|
|
exit 0
|
|
fi
|
|
|
|
jq '.data."freeform-tags" // {}' "$SOURCE_IMAGE_JSON" >"$TEMP_DIR/freeform-tags.json"
|
|
jq '.data."defined-tags" // {}' "$SOURCE_IMAGE_JSON" >"$TEMP_DIR/defined-tags.json"
|
|
|
|
TIMESTAMP="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
SAFE_SOURCE_IMAGE_OCID="${SOURCE_IMAGE_OCID//[^A-Za-z0-9._-]/_}"
|
|
EXPORT_OBJECT_NAME="image-copy-${SAFE_SOURCE_IMAGE_OCID}-${TIMESTAMP}.oci"
|
|
PAR_NAME="image-copy-${SAFE_SOURCE_IMAGE_OCID}-${TIMESTAMP}"
|
|
PAR_EXPIRES_AT="$(utc_after_seconds "$PAR_TTL_SECONDS")"
|
|
|
|
log "Exporting source image to source Object Storage bucket ${SOURCE_EXPORT_BUCKET}"
|
|
oci_source compute image export to-object \
|
|
--image-id "$SOURCE_IMAGE_OCID" \
|
|
--export-format "$EXPORT_FORMAT" \
|
|
--namespace "$SOURCE_NAMESPACE" \
|
|
--bucket-name "$SOURCE_EXPORT_BUCKET" \
|
|
--name "$EXPORT_OBJECT_NAME" \
|
|
>/dev/null \
|
|
2> >(redact_stderr >&2)
|
|
|
|
EXPORT_WORK_REQUEST_ID="$(find_latest_export_work_request)"
|
|
if [[ -n "$EXPORT_WORK_REQUEST_ID" ]]; then
|
|
log "Tracking export work request ${EXPORT_WORK_REQUEST_ID}"
|
|
else
|
|
log "WARNING: could not identify export work request; falling back to object polling only"
|
|
fi
|
|
|
|
wait_for_export_object "$EXPORT_OBJECT_NAME"
|
|
|
|
log "Creating short-lived source ObjectRead PAR"
|
|
PAR_JSON="$(
|
|
oci_source os preauth-request create \
|
|
--namespace-name "$SOURCE_NAMESPACE" \
|
|
--bucket-name "$SOURCE_EXPORT_BUCKET" \
|
|
--name "$PAR_NAME" \
|
|
--access-type ObjectRead \
|
|
--object-name "$EXPORT_OBJECT_NAME" \
|
|
--time-expires "$PAR_EXPIRES_AT" \
|
|
--output json \
|
|
2> >(redact_stderr >&2)
|
|
)"
|
|
PAR_ID="$(jq -r '.data.id // empty' <<<"$PAR_JSON")"
|
|
PAR_ACCESS_URI="$(jq -r '.data."access-uri" // empty' <<<"$PAR_JSON")"
|
|
PAR_JSON=""
|
|
|
|
if [[ -z "$PAR_ID" || -z "$PAR_ACCESS_URI" ]]; then
|
|
fail "PAR creation did not return both id and access URI"
|
|
fi
|
|
PAR_CREATED=1
|
|
|
|
OCI_IMAGE_IMPORT_URI="https://objectstorage.${SOURCE_REGION}.oraclecloud.com${PAR_ACCESS_URI}"
|
|
PAR_ACCESS_URI=""
|
|
|
|
log "Importing image into daaspreproduction compartment"
|
|
if ! TARGET_IMAGE_OCID="$(import_image "$COPY_DEFINED_TAGS")"; then
|
|
if [[ "$COPY_DEFINED_TAGS" == "1" && "$(jq 'length' "$TEMP_DIR/defined-tags.json")" != "0" ]]; then
|
|
log "Import with copied defined tags failed; retrying without defined tags"
|
|
TARGET_IMAGE_OCID="$(import_image "0")"
|
|
else
|
|
fail "target image import failed"
|
|
fi
|
|
fi
|
|
unset OCI_IMAGE_IMPORT_URI
|
|
|
|
if [[ -z "$TARGET_IMAGE_OCID" || "$TARGET_IMAGE_OCID" == "null" ]]; then
|
|
fail "target import did not return an image OCID"
|
|
fi
|
|
|
|
log "Polling target image until AVAILABLE"
|
|
poll_target_image "$TARGET_IMAGE_OCID"
|
|
|
|
delete_source_export_object
|
|
|
|
printf '%s\n' "$TARGET_IMAGE_OCID"
|