#! /usr/bin/env nix-shell #! nix-shell -i bash -p coreutils file gnugrep jq oci-cli openssh terraform # # usage: snatch-instance [PARAM...] # # Obtain a compute instance at OCI, and terminate once successful. # This program might be useful when Oracle is out of host capacity. # # For possible values of PARAM search for "Available parameters" in this file. set -efu main() {( # Available parameters and their default values. # Change parameters via command line using following syntax: # --NAME=VALUE # where NAME is one of param_NAME below. # # Example: --display_name=mycomputer param_apply=true param_availability_domain_list_json= param_display_name=computer param_shape=VM.Standard.A1.Flex param_memory_in_gbs=24 param_ocpus=4 param_ssh_authorized_key_file=$HOME/.ssh/id_rsa.pub param_workdir= param_compartment_id= param_source_id= param_subnet_id= # Set parameters from command line. for arg; do case $arg in --*=*) readonly "param_${arg#--}" ;; *) echo "$0: warning: ignoring unknown argument: $arg" >&2 esac done # # Compute parameter defaults for each unset parameter. # if test -n "$param_apply"; then conf_apply=$param_apply else echo "$0: error: undefined parameter: $param_apply" >&2 exit 2 fi echo "$0: apply: $conf_apply" >&2 if test -n "$param_display_name"; then conf_display_name=$param_display_name else echo "$0: error: undefined parameter: $param_display_name" >&2 exit 2 fi echo "$0: display_name: $conf_display_name" >&2 if test -n "$param_ssh_authorized_key_file"; then if ! is_ssh_public_key "$param_ssh_authorized_key_file"; then echo "$0: error: not an ssh public key: $param_ssh_authorized_key_file" >&2 exit 1 fi conf_ssh_authorized_key=$(cat "$param_ssh_authorized_key_file") else echo "$0: error: undefined parameter: $param_ssh_authorized_key_file" >&2 exit 2 fi if test -n "$param_workdir"; then conf_workdir=$param_workdir mkdir -v -p "$conf_workdir" else conf_workdir=$(mktemp -d -t oci.snatch-instance.$$.XXXXXXXX) trap cleanup EXIT cleanup() { rm -v -R "$conf_workdir" } fi echo "$0: workdir: $conf_workdir" >&2 if test -n "$param_compartment_id"; then conf_compartment_id=$param_compartment_id else conf_compartment_id=$( export response="$( oci iam compartment list \ --lifecycle-state=ACTIVE \ )" jq -enr ' def assert(cond; msg): if cond then . else error(msg) end; env.response | fromjson | .data | assert(length == 1; "could not find exactly one compartment") | .[0] | .["compartment-id"] ' ) fi echo "$0: compartment_id: $conf_compartment_id" >&2 if test -n "$param_availability_domain_list_json"; then conf_availability_domain_list_json=$param_availability_domain_list_json else conf_availability_domain_list_json=$( export response="$( oci iam availability-domain list \ --compartment-id="$conf_compartment_id" )" jq -enr ' env.response | fromjson | .data|map(.name) ' ) export conf_availability_domain_list_json fi echo "$0: availability_domain_list: $(jq -enr ' env.conf_availability_domain_list_json | fromjson | join(", ") ')" >&2 if test -n "$param_shape"; then conf_shape=$param_shape else echo "$0: error: undefined parameter: $param_shape" >&2 exit 2 fi echo "$0: shape: $conf_shape" >&2 if test -n "$param_memory_in_gbs"; then conf_memory_in_gbs=$param_memory_in_gbs else echo "$0: error: undefined parameter: $param_memory_in_gbs" >&2 exit 2 fi echo "$0: memory_in_gbs: $conf_memory_in_gbs" >&2 if test -n "$param_ocpus"; then conf_ocpus=$param_ocpus else echo "$0: error: undefined parameter: $param_ocpus" >&2 exit 2 fi echo "$0: ocpus: $conf_ocpus" >&2 if test -n "$param_source_id"; then conf_source_id=$param_source_id else conf_source_id=$( export response="$( oci compute image list \ --all \ --lifecycle-state=AVAILABLE \ --operating-system='Canonical Ubuntu' \ --compartment-id="$conf_compartment_id" \ --shape="$conf_shape" \ )" jq -enr ' env.response | fromjson | .data | sort_by(.["display-name"]) | last | .id ' ) fi echo "$0: source_id: $conf_source_id" >&2 if test -n "$param_subnet_id"; then conf_subnet_id=$param_subnet_id else conf_subnet_id=$( export response="$( oci network subnet list \ --compartment-id="$conf_compartment_id" \ --lifecycle-state=AVAILABLE \ )" jq -enr ' def assert(cond; msg): if cond then . else error(msg) end; env.response | fromjson | .data | assert(length == 1; "could not find exactly one compartment") | .[0] | .id ' ) fi echo "$0: subnet_id: $conf_subnet_id" >&2 # This tf_config will create a minimal (not applicable) configuration # which is sufficient to initalize terraform plugins. tf_config > "$conf_workdir"/main.tf.json terraform -chdir="$conf_workdir" init attempt=0 until { attempt=$(expr $attempt + 1) availability_domain=$( jq -enr --argjson attempt "$attempt" ' env.conf_availability_domain_list_json | fromjson | .[($attempt - 1) % length] ' ) tf_config \ --availability_domain="$availability_domain" \ --compartment_id="$conf_compartment_id" \ --display_name="$conf_display_name" \ --memory_in_gbs="$conf_memory_in_gbs" \ --ocpus="$conf_ocpus" \ --shape="$conf_shape" \ --source_id="$conf_source_id" \ --ssh_authorized_key="$conf_ssh_authorized_key" \ --subnet_id="$conf_subnet_id" \ > "$conf_workdir"/main.tf.json terraform -chdir="$conf_workdir" plan -out "$conf_workdir"/main.tfplan if test "$conf_apply" = true; then terraform -chdir="$conf_workdir" apply "$conf_workdir"/main.tfplan else echo "$0: Not not applying terraform plan" >&2 fi }; do echo sleeping... >&2 sleep 1m done echo "$0: $(date -Is): success on $attempt. attempt" >&2 )} is_ssh_public_key() { { file -b "$1" | grep -Fq 'public key' && ssh-keygen -l -f "$1" } >/dev/null 2>&1 } # usage: tf_config [PARAM...] # For possible values of PARAM see for "tf_config parameters" below. tf_config() {( # tf_config parameters. # Change parameters via arguments using following syntax: # --NAME=VALUE # where NAME is one of arg_NAME below. # # Example: --display_name=mycomputer arg_availability_domain= arg_compartment_id= arg_display_name= arg_memory_in_gbs= arg_ocpus= arg_shape= arg_source_id= arg_ssh_authorized_key= arg_subnet_id= for arg; do case $arg in --*=*) readonly "arg_${arg#--}" ;; *) echo "$0: warning: ignoring unknown argument: $arg" >&2 esac done nix-instantiate \ --eval --json --strict \ --argstr availability_domain "$arg_availability_domain" \ --argstr compartment_id "$arg_compartment_id" \ --argstr display_name "$arg_display_name" \ --argstr memory_in_gbs "$arg_memory_in_gbs" \ --argstr ocpus "$arg_ocpus" \ --argstr source_id "$arg_source_id" \ --argstr shape "$arg_shape" \ --argstr ssh_authorized_key "$arg_ssh_authorized_key" \ --argstr subnet_id "$arg_subnet_id" \ -E \ ' { availability_domain , compartment_id , display_name , memory_in_gbs , ocpus , shape , source_id , ssh_authorized_key , subnet_id }: { terraform.required_providers.oci = { source = "oracle/oci"; }; provider.oci = {}; resource.oci_core_instance.generated_oci_core_instance = { agent_config = { is_management_disabled = "true"; is_monitoring_disabled = "true"; plugins_config = [ { desired_state = "DISABLED"; name = "Vulnerability Scanning"; } { desired_state = "DISABLED"; name = "Compute Instance Monitoring"; } { desired_state = "DISABLED"; name = "Bastion"; } ]; }; availability_config = { is_live_migration_preferred = "true"; recovery_action = "RESTORE_INSTANCE"; }; availability_domain = availability_domain; compartment_id = compartment_id; create_vnic_details = { assign_private_dns_record = "true"; assign_public_ip = "true"; subnet_id = subnet_id; }; display_name = display_name; instance_options = { are_legacy_imds_endpoints_disabled = "false"; }; metadata = { ssh_authorized_keys = ssh_authorized_key; }; shape = shape; shape_config = { memory_in_gbs = memory_in_gbs; ocpus = ocpus; }; source_details = { source_id = source_id; source_type = "image"; }; }; } ' )} main "$@"