Skip to content

Repository files navigation

OCI Free Tier Retry

PowerShell License Release PSScriptAnalyzer GitHub last commit Issues Stars

PowerShell 7+ retry engine that keeps calling oci compute instance launch until Oracle Cloud has Always Free ARM (VM.Standard.A1.Flex) capacity, rotating through every availability domain in the compartment, then waits for the instance to boot and reports its public IP.

Prerequisites

  • OCI CLI installed and already configured (oci setup config already run, oci iam region list already works).

  • PowerShell 7+ (pwsh). The script enforces this with #Requires -Version 7.0.

  • An SSH key pair at $HOME\.ssh\oracle_ssh / $HOME\.ssh\oracle_ssh.pub. Generate one if it doesn't exist:

    ssh-keygen -t ed25519 -f "$HOME\.ssh\oracle_ssh"

Project layout

Start-Retry.ps1      Entry point / retry loop
config.example.ps1    Template config, tracked in git -- placeholders only
config.local.ps1       Your real config -- git-ignored, never committed
OracleCli.ps1          oci CLI wrapper, launch-request builder, error classifier
Logger.ps1             Timestamped logging to logs/*.log
LaunchTemplate.json    Skeleton instance-launch request body
logs/                   retry.log, error.log, success.log
output/                 launch.json (generated per attempt), last-success.json

Configuration

Start-Retry.ps1 loads config.local.ps1, which is git-ignored so your real OCIDs never get committed. Set it up once:

Copy-Item config.example.ps1 config.local.ps1
notepad config.local.ps1

If config.local.ps1 is missing, the script fails immediately with a message pointing you at this step -- it won't run against config.example.ps1's placeholders. In config.local.ps1, set:

  • CompartmentId, SubnetId -- required, no default will work for your tenancy.
  • ImageId -- leave blank to auto-resolve the newest matching image via oci compute image list at startup (uses OperatingSystem / OperatingSystemVersion), or pin a specific image id for reliability.
  • Shape, Ocpus, MemoryInGBs, BootVolumeSizeInGBs -- Always Free ARM ceiling is 4 OCPUs / 24 GB total across all A1 instances in a tenancy.
  • RetryIntervalSeconds, RateLimitBackoffSeconds -- pacing between attempts.
  • SshPublicKeyPath / SshPrivateKeyPath -- default to oracle_ssh(.pub).

Availability domains are not hardcoded: they're fetched at startup via oci iam availability-domain list, so the same script works in any region regardless of how many ADs it has.

Running

pwsh -File .\Start-Retry.ps1

The script retries forever (Ctrl+C to stop) until it successfully launches and the instance reaches RUNNING with a public IP, then prints:

==================== INSTANCE READY ====================
Instance Name : portfolio-vm-20260719-141502
Instance OCID : ocid1.instance.oc1...
Public IP     : 130.x.x.x
Private IP    : 10.x.x.x
SSH Command   : ssh -i "C:\Users\you\.ssh\oracle_ssh" ubuntu@130.x.x.x
==========================================================

Why --from-json instead of --shape-config / --metadata flags

The original PowerShell attempt passed --shape-config as an inline JSON string on the command line, escaped with backticks:

--shape-config "{`"ocpus`":$Ocpus,`"memoryInGBs`":$Memory}"

This works in Bash but is unreliable on Windows: PowerShell and the Win32 process-creation layer both re-quote arguments before an external executable sees them, and that re-quoting can strip or corrupt embedded " characters, so the CLI sometimes receives malformed JSON instead of the intended object.

To avoid that entire bug class, this project never puts JSON on the command line. Every launch attempt builds a complete request object in memory from LaunchTemplate.json (field names/casing match oci compute instance launch --generate-full-command-json-input), writes it to output\launch.json via ConvertTo-Json (which handles all escaping), and submits it with:

oci compute instance launch --from-json file://<absolute path to launch.json> --output json

Only a plain file path ever crosses the command-line boundary.

Two related things discovered while validating this end-to-end against a real tenancy, both handled in OracleCli.ps1:

  • --from-json still needs the top-level convenience keys, not just the nested complex objects. oci compute instance launch --generate-full-command-json-input shows both a nested createVnicDetails.subnetId and a flat top-level subnetId key. Only the top-level one satisfies the CLI's own required-option check -- createVnicDetails.subnetId alone still fails with Missing option(s) --subnet-id. LaunchTemplate.json sets both to be safe.
  • --no-retry is passed on every invocation. The OCI Python SDK has its own built-in retry strategy for retryable 5xx errors -- which includes "Out of host capacity", the exact routine case this whole engine exists to handle -- and without --no-retry it silently retries inside a single CLI call for 60-100+ seconds before ever returning. That fights this script's own AD-rotation retry loop and makes RetryIntervalSeconds meaningless. Invoke-OciCli is the single call site for the oci CLI, so this is applied centrally rather than repeated at every call site.
  • Every oci CLI call also gets a hard local timeout (Config.OciCommandTimeoutSeconds, default 120s) via System.Diagnostics.Process with async stream reads, so a single stuck invocation can never block the infinite retry loop forever.
  • On Windows, the CLI additionally shells out to a nested PowerShell process to Get-Acl the ~/.oci/config file as a permissions check on every invocation. Invoke-OciCli sets OCI_CLI_SUPPRESS_FILE_PERMISSIONS_WARNING=True on the child process to skip that (it only silences an informational warning, it doesn't touch actual credential/config file security) -- and, defensively, Remove-OciCliNoise strips that PowerShell error scaffolding from any output that does slip through before it's classified, since generic tokens in it (e.g. CommandNotFoundException) would otherwise be misread as a genuine OCI "NotFound" API error and wrongly treated as fatal.

Retry logic

  • Availability domains are tried round-robin, one attempt per AD per cycle.
  • Every oci CLI invocation's combined stdout+stderr is classified:
    • Capacity ("Out of host capacity") -- log and rotate to the next AD.
    • RateLimit (TooManyRequests) -- back off for RateLimitBackoffSeconds.
    • Network (connection/timeout errors) -- log and retry.
    • Auth (authentication/authorization failures) -- fatal, stops the script (retrying with the same broken credentials can't help).
    • InvalidJson / InvalidConfig (malformed request, bad/missing parameter, 404 on a referenced resource) -- fatal, stops the script, since retrying a structurally broken request forever is pointless.
    • Anything else -- logged as an unknown CLI failure and retried.
  • If OCI accepts the launch but the instance later lands in TERMINATED (an asynchronous capacity fault), the loop treats that the same as a capacity error and retries with the next AD.
  • On success, the script polls until the instance is RUNNING and has a public IP before printing results and exiting 0.

Logs

  • logs\retry.log -- everything, timestamped.
  • logs\error.log -- ERROR-level lines only.
  • logs\success.log -- SUCCESS-level lines only.
  • output\launch.json -- the exact request body sent on the most recent attempt.
  • output\last-success.json -- instance id/IPs from the last successful run.

Versioning & releases

The current version lives in the VERSION file and is logged on every run (Starting OCI Always Free ARM retry engine vX.Y.Z.). This project follows Semantic Versioning; notable changes are tracked in CHANGELOG.md (Keep a Changelog format).

To cut a release:

  1. Bump VERSION and move the relevant [Unreleased] entries in CHANGELOG.md under a new dated ## [X.Y.Z] - YYYY-MM-DD section.

  2. Commit and push that to main.

  3. Tag and push the tag:

    git tag vX.Y.Z
    git push origin vX.Y.Z

.github/workflows/release.yml then publishes a GitHub Release automatically, using that CHANGELOG.md section as the release notes. It fails loudly if the tag doesn't match VERSION or if no matching changelog section is found -- both catch a forgotten bump before a broken release goes out.

Contributing

See CONTRIBUTING.md for dev setup, code style, and how to submit a PR.

About

PowerShell 7 retry engine that keeps hammering oci compute instance launch across every availability domain until Oracle gives up an Always Free ARM (A1.Flex) instance — then reports its IP.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages