#!/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 <&2 } fail() { log "ERROR: $*" exit 1 } redact_stderr() { sed -E \ -e 's#https://objectstorage\.[^[:space:]"'"'"']+##g' \ -e 's#/p/[-A-Za-z0-9_./?=%&:+]+##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"