Skip to content

Repository files navigation

OCI Capacity-Aware Provisioner

CI Python 3.12+ License: MIT Release

A safety-first, capacity-aware OCI compute provisioner that handles temporary host-capacity shortages with bounded retries, idempotency, cost guardrails, and duplicate-instance prevention.


The problem

Creating a VM.Standard.A1.Flex instance in a popular Oracle Cloud region usually fails on the first try:

ServiceError: 500 InternalError — Out of host capacity.

This is not a misconfiguration. Oracle simply has no free Ampere host in that availability domain at that moment, and it will have one later. The obvious response — a shell loop that retries every ten seconds — is a bad one. It gets the tenancy throttled, it creates duplicate instances when a request times out after the server already accepted it, and left running overnight it can quietly provision resources outside the free allowance.

What this does instead

Rotates availability domains One request at a time, moving to the next domain on a capacity shortage rather than retrying the same one.
Waits, deliberately 10 minutes between rounds by default, with jitter. The floor is 5 minutes and is not configurable.
Stops on success, immediately The first RUNNING instance ends the run.
Never creates a duplicate Checks for a live instance with the same display name before every launch request, and uses an OCI retry token so a timed-out request cannot land twice.
Refuses to overspend Totals the tenancy's existing Ampere usage and refuses to send a request that would exceed the Always Free allowance.
Never falls back to a paid shape The shape allowlist has exactly one entry.
Explains itself doctor finds the private subnet, the x86 image, or the missing IAM policy in five seconds, instead of forty minutes into a retry loop.
Shuts down cleanly Ctrl-C saves state, releases the lock, and exits 130.

Safety guardrails

Guardrail How it is enforced Can a user turn it off?
Only VM.Standard.A1.Flex One-element allowlist checked at config parse and before each launch No
Max 2 A1 OCPUs tenancy-wide Measured against live OCI state before every run No
Max 12 GB A1 memory tenancy-wide Same No
Max 200 GB block storage Boot and block volumes totalled across all availability domains No
Boot volume 50–200 GB Validated against the request No
Minimum 300 s retry interval Rejected at config validation, clamped in code No
Bounded run time max_runtime_hours, capped at 168 Within the cap
One process per machine OS file lock No
One request at a time No concurrency anywhere in the engine No
Fail closed on unknown errors Unrecognised errors are fatal by design No
Fail closed on unmeasurable usage If usage cannot be totalled, no request is sent No

There is no --allow-paid, no --allow-over-free-tier, and no environment variable that relaxes any of the above. Changing a ceiling requires editing policies.py and cutting a release. See ADR 0003 for why.

This tool is not a "capacity sniper". It sends slow, bounded, sequential requests. It does not bypass quotas, does not run parallel requests, and does not poll faster than once every five minutes.


Installation

Requires Python 3.12 or newer. macOS and Linux are supported; Windows works under WSL on a best-effort basis.

pipx install git+https://github.com/its-spark-dev/oci-capacity-aware-provisioner
Other installation methods
# uv
uv tool install git+https://github.com/its-spark-dev/oci-capacity-aware-provisioner

# From a release wheel
pip install ./oci_capacity_aware_provisioner-0.1.0-py3-none-any.whl

# From a source checkout
git clone https://github.com/its-spark-dev/oci-capacity-aware-provisioner
cd oci-capacity-aware-provisioner
pipx install .

pipx and uv tool are recommended for everyday use: they keep the tool and its dependencies out of your system Python.

Verify:

oci-capacity-provisioner version

Quick start

oci-capacity-provisioner init              # 1. create config.toml interactively
oci-capacity-provisioner doctor            # 2. check the environment
oci-capacity-provisioner launch --dry-run  # 3. see the exact request
oci-capacity-provisioner launch            # 4. provision, retrying patiently
oci-capacity-provisioner status            # 5. see what exists

Try it without an Oracle Cloud account first:

make install && make demo

The demo runs the whole flow — including a capacity shortage in two availability domains — against an in-process fake. No credentials, no network, nothing created.


OCI API authentication

The tool uses OCI API key authentication and reads ~/.oci/config. It never reads, copies, or logs your private key.

1. Generate an API signing key

mkdir -p ~/.oci
openssl genrsa -out ~/.oci/oci_api_key.pem 2048
chmod 600 ~/.oci/oci_api_key.pem
openssl rsa -pubout -in ~/.oci/oci_api_key.pem -out ~/.oci/oci_api_key_public.pem

2. Upload the public key in the OCI Console

  1. Sign in to the OCI Console.
  2. Open the profile menu (top right) and choose My profile.
  3. Under Resources, select API keys.
  4. Click Add API key, choose Paste a public key, and paste the contents of ~/.oci/oci_api_key_public.pem.
  5. Click Add. The Console shows a configuration file preview containing your user, fingerprint, tenancy, and region. Copy it.

3. Write ~/.oci/config

Paste the preview and add the path to your private key:

[DEFAULT]
user=ocid1.user.oc1..aaaaaaaayour-user-ocid
fingerprint=aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99
tenancy=ocid1.tenancy.oc1..aaaaaaaayour-tenancy-ocid
region=us-ashburn-1
key_file=~/.oci/oci_api_key.pem
chmod 600 ~/.oci/config

The fingerprint must match the key in key_file. If a launch later fails with NotAuthenticated, this is the first thing to check — compute it from your own key and compare against what the Console shows:

openssl rsa -pubout -outform DER -in ~/.oci/oci_api_key.pem 2>/dev/null | openssl md5 -c

4. Confirm it works

oci-capacity-provisioner doctor

Oracle's full walkthrough: Required Keys and OCIDs.

Required IAM permissions

The user needs to inspect and create compute in the target compartment. A minimal policy:

Allow group <your-group> to manage instance-family in compartment <your-compartment>
Allow group <your-group> to use subnets in compartment <your-compartment>
Allow group <your-group> to use vnics in compartment <your-compartment>
Allow group <your-group> to read virtual-network-family in compartment <your-compartment>
Allow group <your-group> to inspect volume-family in compartment <your-compartment>
Allow group <your-group> to read instance-images in compartment <your-compartment>

The optional Compute Capacity Report additionally needs manage compute-capacity-reports. Without it the tool warns once and carries on unchanged.


Commands

init

Creates config.toml, discovering real values from your tenancy.

oci-capacity-provisioner init                    # interactive

oci-capacity-provisioner init --non-interactive \
    --region us-ashburn-1 \
    --compartment-id ocid1.compartment.oc1..aaaa... \
    --subnet-id ocid1.subnet.oc1.iad.aaaa... \
    --ssh-public-key-path ~/.ssh/id_ed25519.pub

It lists your compartments and subnets — marking which subnets permit public IPs — confirms a compatible Arm image exists, writes the file with 0600 permissions, and validates it. It never copies credentials or private key material into the file.

doctor

oci-capacity-provisioner doctor
oci-capacity-provisioner doctor --offline   # no network at all

Checks Python and SDK versions, authentication, region and home region, compartment access, subnet existence and VCN linkage, whether the subnet permits public IPs, internet gateway routing, Arm image resolution, SSH key format, shape availability, current A1 OCPU / memory / storage usage, the projected allowance, availability domains, duplicate instances, the lock, prior run state, and webhook configuration.

Each check is PASS, WARN, FAIL, or SKIP. A single FAIL blocks launch. Every failure comes with a remedy.

validate-config

oci-capacity-provisioner validate-config            # static, no network
oci-capacity-provisioner validate-config --online   # also verifies OCI resources

launch --dry-run

oci-capacity-provisioner launch --dry-run

Resolves everything a real launch would — image, availability domains, current usage, duplicates — and prints the exact request instead of sending it. Identifiers are masked; the SSH key is never printed.

launch --once

oci-capacity-provisioner launch --once

Tries each availability domain at most once, then stops. Good for a cron job that nudges rather than camps. Exit code 5 means "no capacity this time".

launch

oci-capacity-provisioner launch

Runs until it succeeds or max_runtime_hours elapses. Prints the next attempt time between rounds. On success it prints the public IP and an SSH command.

To stop it: press Ctrl-C, or kill -TERM <pid>. Either way it finishes the in-flight API call, saves state, releases the lock, and exits 130. Do not kill -9 it — that skips the cleanup.

status

oci-capacity-provisioner status

Asks OCI directly for the instance's lifecycle state, availability domain, shape, resources, masked OCID, addresses, and creation time.

version

oci-capacity-provisioner version

Exit codes

Code Meaning
0 Success, or a safe no-op (the instance already exists)
1 Unrecoverable error
2 Bad command-line usage
3 Configuration missing, malformed, or invalid
4 A pre-flight check or guardrail refused the launch
5 Ran out of time while still hitting capacity shortages
6 Another provisioner process holds the lock
130 Interrupted by SIGINT or SIGTERM

Configuration

See config.example.toml for a fully annotated file, and examples/openclaw.toml for a worked example.

[oci]

Key Default Meaning
profile "DEFAULT" Profile name in ~/.oci/config
region Region identifier, e.g. us-ashburn-1
compartment_id Compartment (or tenancy) OCID
subnet_id Subnet OCID, in the same region

[image]

Key Default Meaning
image_id "" Pin a specific image. Leave empty — image OCIDs are region-specific
operating_system "Canonical Ubuntu" Must match Oracle's platform image name exactly
operating_system_version "24.04"
architecture "aarch64" Only aarch64 is accepted
prefer_latest true Prefer the newest matching image

[instance]

Key Default Meaning
display_name "oci-a1-instance" Also the duplicate-detection key
shape "VM.Standard.A1.Flex" The only permitted value
ocpus 2
memory_gb 12
boot_volume_gb 50 50–200
assign_public_ip true Requires a public subnet
ssh_public_key_path "~/.ssh/id_ed25519.pub" Public key only

[retry]

Key Default Meaning
interval_seconds 600 Minimum 300, enforced
max_runtime_hours 48 Maximum 168
jitter_seconds 30 One-sided; never shortens the wait
availability_domains [] Empty means discover at runtime

[logging]

Key Default Meaning
level "INFO" DEBUG, INFO, WARNING, ERROR
json_log_path "./logs/provisioner.jsonl" "" disables the audit trail

[notification]

Key Default Meaning
enabled false
timeout_seconds 10
notify_on_success true
notify_on_fatal true
notify_on_timeout true

The webhook URL is not a config key. It is read from OCI_CAPACITY_WEBHOOK_URL, because webhook URLs usually embed a secret and config files get pasted into bug reports.

Precedence

CLI flags → environment variables → config.toml → built-in defaults.

Variable Overrides
OCI_CAPACITY_CONFIG Config file location
OCI_PROFILE [oci].profile
OCI_REGION [oci].region (also read by the OCI SDK)
OCI_COMPARTMENT_ID [oci].compartment_id
OCI_SUBNET_ID [oci].subnet_id
OCI_IMAGE_ID [image].image_id
OCI_SSH_PUBLIC_KEY_PATH [instance].ssh_public_key_path
OCI_CAPACITY_WEBHOOK_URL The webhook URL (no config equivalent)
OCI_CAPACITY_STATE_DIR Where state and the lock live

Config file search order: --config$OCI_CAPACITY_CONFIG./config.toml$XDG_CONFIG_HOME/oci-capacity-aware-provisioner/config.toml.


Cost safety

Read this before running anything.

  • This tool does not guarantee Oracle's Always Free policy. It enforces the ceilings it was built with; Oracle owns the actual policy and can change it at any time, without changing this tool.
  • Check the official documentation before you run it: Always Free Resources.
  • On a pay-as-you-go tenancy, anything beyond the free allowance is billed. A PAYG account will happily create a paid resource. The guardrails here are your seatbelt, not Oracle's.
  • Review your own tenancy limits and estimated cost in the OCI Console before a long unattended run.

What the tool enforces, as of 0.1.0:

  • VM.Standard.A1.Flex only. No fallback to any other shape, ever.
  • 2 A1 OCPUs and 12 GB of A1 memory in total across the tenancy — Oracle's documented Always Free Ampere allowance.
  • 200 GB of combined boot and block volume storage.
  • Boot volumes between 50 GB and 200 GB.
  • No additional block volumes, no dedicated hosts, no capacity reservations, no preemptible instances.

Before each run it lists every non-terminated A1 instance and every live volume across every compartment your user can see, totals them, adds the request, and compares against those ceilings. If it cannot complete that measurement — a permission error, an empty availability domain list — it refuses to launch. Not knowing is never treated as zero.

The ceilings are tenancy-wide totals, not per-instance

2 OCPUs and 12 GB is what your whole tenancy may hold, not what one instance may be. So:

  • One 2 OCPU / 12 GB instance uses the entire allowance.

  • Two 1 OCPU / 6 GB instances also use the entire allowance, and are a perfectly valid way to split it — set ocpus = 1 and memory_gb = 6 in config.toml.

  • If you already have an A1 instance using the allowance, launch will refuse to create another. That is correct behaviour, not a bug. It refuses even when the existing instance has a completely different name, and even when it is STOPPED — a stopped instance still holds its allocation. doctor shows you the projection:

    FAIL  allowance.projection   Projected A1 OCPUs 4 would exceed the Always Free
                                 allowance of 2 (currently 2 in use across 1 instance(s)).
    

    To proceed, terminate the existing instance yourself, or lower ocpus and memory_gb so the total fits.

  • If your user cannot list every compartment in the tenancy, doctor reports allowance.scope: WARN and says so. The totals may then undercount, because an A1 instance in a compartment you cannot see still consumes the allowance.

What this tool does not do

  • It never terminates or deletes anything. Existing instances, boot volumes, and block volumes are read and counted, never touched. Cleaning up is yours to do in the Console.
  • It does not watch for cost after the instance exists. The guardrails run before a launch; nothing runs afterwards.
  • A budget does not stop billing. Set one in the OCI Console under Governance & Administration → Budgets — but understand that an OCI budget only sends an alert when a threshold is crossed. It does not block, cap, or prevent charges. Nothing in OCI does. A budget tells you a bill is coming; it does not stop it.
  • A successful capacity report is not a guarantee. It does not reserve anything, and a launch immediately afterwards can still fail.

Retry policy

Two independent retry layers, kept separate on purpose (ADR 0002):

  • Transport retries — the OCI SDK's own strategy, retrying a single HTTP call over a dropped connection within seconds.
  • Capacity retries — this tool, deciding when to try the next launch, on a scale of minutes.

LaunchInstance explicitly opts out of the SDK's strategy. Oracle reports a capacity shortage as HTTP 500 with the service code InternalError, which the SDK's default checker treats as a retryable 5xx — leaving it enabled would fire up to eight launch attempts inside what the tool reports as one, and defeat the availability-domain rotation.

Why 300 seconds is the floor

Ampere capacity is freed by other tenants terminating instances, on a timescale of minutes to hours. Polling every ten seconds does not detect that sooner; it just multiplies request volume by thirty, invites HTTP 429, and degrades the control plane for everyone. The floor is validated at config load and clamped in code. There is no flag to lower it.

What is retried, and what is not

Retryable Fatal
Out of host capacity (500 InternalError) Authentication failure (401)
Throttling (429) Permission denied (403, 404 NotAuthorizedOrNotFound)
Transient 5xx Service limit or quota exceeded (LimitExceeded, QuotaExceeded)
Connection reset, DNS failure, timeout* Billing or subscription problems
Invalid parameters, invalid shape configuration
SSH key errors, guardrail violations
Anything unrecognised

* A timeout is not retried blindly. The launch may have succeeded server-side, so the tool queries OCI for the instance before deciding what to do.

Classification uses the exception type, HTTP status, and the OCI service error code together — never a message substring alone. Message text only narrows codes OCI reuses for several conditions. Unknown errors are fatal by design: retrying something nobody understood is how a careful tool becomes a hammer.


Idempotency and duplicate prevention

OCI display names are not unique. Nothing stops two LaunchInstance calls from producing two instances called oci-a1-instance, each consuming half your free allowance.

Three mechanisms prevent that (ADR 0004):

  1. A pre-launch query before every request. Not once at startup — before each individual launch, because a run can last two days. Any instance not in TERMINATED counts, including STOPPED. If the query itself fails, no request is sent.
  2. An OCI retry token. Every request carries opc-retry-token, derived deterministically from the request's identity and the target availability domain. A timed-out request repeated with the same token returns the original instance instead of creating a second. The token rotates only when it expires (20 hours, inside Oracle's 24-hour window) or when OCI rejects it.
  3. A machine-wide lock. An OS file lock, released by the kernel if the process dies.

Local state is recorded, but never trusted as the answer: the Compute API is.


Compute Capacity Report

When the tenancy has permission, the tool asks OCI's Compute Capacity Report which availability domains report free capacity, and tries those first.

The report is a hint, not a reservation. It does not hold capacity, it can be stale by the time the launch is sent, and it can be wrong in both directions. So it is only ever used to reorder a round — never to skip a launch attempt, and never as a substitute for actually trying. If the call fails or the permission is missing, the tool warns once and behaves exactly as it would without it.

Disable it with --no-capacity-report.


After a successful launch

Instance ready.
  display name        : oci-a1-instance
  lifecycle state     : RUNNING
  availability domain : Uocm:US-ASHBURN-AD-3
  shape               : VM.Standard.A1.Flex
  resources           : 2 OCPU / 12 GB
  instance OCID       : ocid1.instance...4f21c8
  private IP          : 10.0.0.42
  public IP           : 203.0.113.42

  ssh ubuntu@203.0.113.42

ubuntu is the default user for Canonical Ubuntu images. If the matching private key is not your SSH default:

ssh -i ~/.ssh/id_ed25519 ubuntu@203.0.113.42

If the connection times out, the instance is running but unreachable — check the subnet's security list allows inbound TCP 22, and that the VCN has an internet gateway with a 0.0.0.0/0 route. doctor warns about the routing case.


Running in the background

A long run should survive a closed laptop lid. See docs/background-running.md for full recipes.

macOS: your Mac will sleep and the run will stall. Use caffeinate:

caffeinate -is oci-capacity-provisioner launch

Linux with systemd — a user service that restarts on failure:

systemctl --user start oci-a1-provisioner
journalctl --user -u oci-a1-provisioner -f

Anywheretmux or screen:

tmux new -s a1 'oci-capacity-provisioner launch'
tmux attach -t a1

nohup ... & works too, but nothing then delivers SIGINT for a clean shutdown; send SIGTERM instead of killing the process.


Logs

Console — human-readable progress: the current round, availability domain, result, and the next attempt time.

File — JSON Lines at ./logs/provisioner.jsonl by default, one object per event, UTC ISO-8601 timestamps:

{"event": "launch.attempt", "round": 2, "attempt": 4, "availability_domain": "Uocm:US-ASHBURN-AD-1", "compartment_id": "ocid1.compartm...b3f9a1", "shape": "VM.Standard.A1.Flex", "ocpus": 2, "timestamp": "2026-03-04T11:20:31.442+00:00", "level": "info"}
{"event": "launch.failed", "round": 2, "error_category": "CAPACITY", "oci_error_code": "InternalError", "status_code": 500, "retryable": true, "request_id": "A1B2C3D4...9F0E", "timestamp": "2026-03-04T11:20:33.887+00:00", "level": "warning"}
{"event": "round.sleeping", "round": 2, "delay_seconds": 612.4, "next_attempt_at": "2026-03-04T11:30:46+00:00", "timestamp": "2026-03-04T11:20:34.001+00:00", "level": "info"}
{"event": "run.succeeded", "instance_id": "ocid1.instance...4f21c8", "availability_domain": "Uocm:US-ASHBURN-AD-3", "public_ip": "203.0.113.42", "attempts": 7, "rounds": 3, "timestamp": "2026-03-04T11:41:02.310+00:00", "level": "info"}

Every field passes through one redaction module. OCIDs are masked to head and tail; request IDs are truncated; private keys, webhook URLs, and authorization headers never appear at all.

Run state lives in ~/.local/state/oci-capacity-aware-provisioner/ (or $XDG_STATE_HOME). It holds identifiers, timings, and the derived idempotency token — never key material, never the webhook URL, never a copy of your config.


Architecture

flowchart TB
    CLI["cli.py<br/><i>commands, exit codes</i>"]
    Doctor["doctor.py<br/><i>environment diagnosis</i>"]
    Prov["provisioner.py<br/><i>the state machine</i>"]

    subgraph Policy["Policy — pure, no I/O"]
        Pol["policies.py<br/><i>free-tier ceilings</i>"]
        Retry["retry_policy.py<br/><i>pacing, AD rotation</i>"]
        Err["errors.py<br/><i>error classification</i>"]
    end

    subgraph Support["Support"]
        State["state.py<br/><i>run state, retry tokens</i>"]
        Lock["locking.py<br/><i>machine-wide lock</i>"]
        Redact["redaction.py<br/><i>central masking</i>"]
        Notify["notifications.py"]
    end

    GW["oci_gateway.py<br/><i>OciGateway protocol</i>"]
    Real["OciSdkGateway<br/><i>real OCI SDK</i>"]
    Fake["FakeOciGateway<br/><i>scripted, in-memory</i>"]
    OCI(["Oracle Cloud<br/>Infrastructure"])

    CLI --> Doctor
    CLI --> Prov
    Doctor --> GW
    Prov --> Policy
    Prov --> Support
    Prov --> GW
    GW --> Real --> OCI
    GW -.tests only.-> Fake

    style Policy fill:#eef7ee,stroke:#4a7a4a
    style GW fill:#eef2fb,stroke:#4a5f8a
    style OCI fill:#fdf3e7,stroke:#a97a3a
Loading

Everything above oci_gateway.py speaks to a narrow protocol expressed in this project's own types. The guardrails, the retry state machine, and the error classifier are therefore fully testable without a tenancy, a network, or a credential — which is what makes the test suite possible.

More detail: docs/architecture.md.

Provisioning state machine

stateDiagram-v2
    [*] --> Preflight

    Preflight --> Blocked: guardrail violated<br/>or check failed
    Preflight --> AlreadyExists: same-named instance found
    Preflight --> Round: all gates passed

    Round --> Attempt: next availability domain
    Attempt --> DuplicateCheck

    DuplicateCheck --> AlreadyExists: live instance found
    DuplicateCheck --> Blocked: check failed — never launch blind
    DuplicateCheck --> Launch: clear

    Launch --> Running: accepted
    Launch --> Attempt: CAPACITY — next domain
    Launch --> Backoff: THROTTLE / transient
    Launch --> Reconcile: TIMEOUT / token rejected
    Launch --> Fatal: anything else

    Backoff --> Launch: bounded short retry
    Reconcile --> AlreadyExists: instance exists after all
    Reconcile --> Launch: nothing was created

    Attempt --> Wait: every domain tried
    Wait --> Round: interval elapsed
    Wait --> Exhausted: budget spent
    Wait --> Interrupted: SIGINT / SIGTERM

    Running --> [*]: exit 0
    AlreadyExists --> [*]: exit 0
    Blocked --> [*]: exit 4
    Exhausted --> [*]: exit 5
    Fatal --> [*]: exit 1
    Interrupted --> [*]: exit 130
Loading

Troubleshooting

Out of host capacity

Not a bug — the condition this tool exists to handle. Oracle has no free Ampere host in that availability domain right now. Let launch keep running; it will rotate domains and retry patiently. Popular regions can take hours or days.

If it never succeeds within max_runtime_hours, try a different region, or a smaller ocpus / memory_gb (1 OCPU / 6 GB fits into gaps that 2 / 12 cannot).

NotAuthorizedOrNotFound

OCI returns this both for "you may not" and "it does not exist", deliberately, so it does not leak which. Check, in order:

  1. The compartment OCID is correct and your user has a policy for it.
  2. The subnet is in the same region as [oci].region — subnet OCIDs are region-specific.
  3. Your group has the permissions listed under Required IAM permissions.

doctor narrows this down: it tells you which specific resource it could not read.

LimitExceeded

A service limit or compartment quota, not a capacity shortage — waiting will not help, so the tool stops immediately.

Check Governance & Administration → Limits, Quotas and Usage in the Console, filter by "Compute", and look at standard-a1-core-count. On a new tenancy this is often 0 until the account finishes provisioning. On a free-tier account it is capped at the Always Free allowance.

InvalidParameter

Usually one of: a shape configuration outside what VM.Standard.A1.Flex allows, a boot volume smaller than the image's default, or an image that cannot boot on this shape. Run doctor, which validates each of these separately.

The instance has no public IP

Either assign_public_ip was false, or the subnet prohibits public IPs on VNICs. doctor fails on the second case before launching. Check with:

oci-capacity-provisioner doctor
I picked a private subnet

doctor reports network.public_ip: FAIL and refuses to launch. Either select a public subnet, or set assign_public_ip = false and reach the instance through a bastion or VPN.

The image is not aarch64

VM.Standard.A1.Flex is an Arm shape and cannot boot an x86 image. Leave image_id empty so a compatible image is resolved automatically. If you pinned one, doctor reports image.resolution: FAIL and names the incompatibility.

The image OCID is from another region

Image OCIDs are region-specific — the single most common reason a config that worked in one region fails in another. The tool detects it and says so. Leave image_id empty to make your config portable.

SSH key format errors
  • is a PRIVATE key, not a public key — point ssh_public_key_path at the .pub file. Never upload the private key anywhere.
  • not valid base64 — the file was wrapped or truncated in transit; re-copy it.
  • contains N keys — supply a file with exactly one key.
  • No key at all: ssh-keygen -t ed25519.
"Your account upgrade is in progress"

A tenancy-level state, treated as fatal. Instance creation is blocked until Oracle finishes the upgrade — usually minutes, occasionally hours. Wait, then re-run doctor.

A stale lock

The lock is an OS file lock, so the kernel releases it when the process dies — a genuinely stale lock is rare. If you see one:

oci-capacity-provisioner status

doctor compares the recorded PID and its command line against what is actually running, and tells you which. Only delete ~/.local/state/oci-capacity-aware-provisioner/provisioner.lock after confirming no instance is being created — never while a run is in progress.

An existing instance was detected

Working as intended. A live instance with the same display_name makes launch a no-op that exits 0. To provision a second instance, change display_name — but check the allowance first; two 2-OCPU instances exceed it.

"Projected A1 OCPUs would exceed the Always Free allowance" — but I only want one instance

Also working as intended, and the most common surprise. The ceiling is a tenancy-wide total, so an A1 instance you already have — under any name, in any compartment you can see, even STOPPED — consumes it.

oci-capacity-provisioner doctor      # shows current usage and the projection
oci-capacity-provisioner status      # shows the instance this config targets

Then either terminate the existing instance in the Console (and delete its boot volume), or split the allowance: set ocpus = 1 and memory_gb = 6 so two instances fit.

There is no flag to raise the ceiling. See ADR 0003.

doctor says allowance.scope: WARN

Your user cannot enumerate every compartment in the tenancy, so the usage totals are measured over only the compartments it can see. Since the allowance is tenancy-wide, the totals may undercount, and a launch that looks safe might take the tenancy over.

The tool still runs — locking out every user on a restricted IAM policy would be worse — but it tells you rather than pretending the narrower number is the whole picture. To clear it, ask for:

Allow group <your-group> to inspect compartments in tenancy
Allow group <your-group> to inspect instance-family in tenancy
Allow group <your-group> to inspect volume-family in tenancy

Or check the compartments you cannot see yourself before launching.

Webhook failures

Logged as webhook.failed and otherwise ignored. A notification failure never changes a provisioning outcome. Confirm OCI_CAPACITY_WEBHOOK_URL is set and that the receiver accepts a JSON POST.

Block storage limit exceeded

The Always Free allowance is 200 GB of boot and block volumes combined, across the tenancy. Terminated instances free their boot volumes, but detached boot volumes persist and still count. Check Storage → Block Volumes and Storage → Boot Volumes in the Console and delete what you no longer need.


Security

  • Credentials are read from ~/.oci/config by the OCI SDK. This tool never copies, stores, or logs private key material.
  • Generated files are written with 0600 permissions.
  • Every log line, webhook payload, and state file goes through one redaction module.
  • The webhook URL is environment-only, never a config field.
  • State contains identifiers and timings, never secrets.

Reporting a vulnerability: SECURITY.md. Never paste a credential into a public issue. If you already have, revoke the API key in the OCI Console immediately.


Contributing

See CONTRIBUTING.md. In short: make install, then make check before opening a pull request. Tests never touch a real tenancy unless you explicitly opt in with two separate environment variables.

Also: CODE_OF_CONDUCT.md · CHANGELOG.md · Architecture decisions

License

MIT © 2026 Sanghyeon Park

This project is not affiliated with, endorsed by, or sponsored by Oracle Corporation. "Oracle Cloud Infrastructure" and "Ampere" are trademarks of their respective owners.

About

A safety-first, capacity-aware OCI compute provisioner that handles temporary host-capacity shortages with bounded retries, idempotency, cost guardrails, and duplicate-instance prevention.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages