A safety-first, capacity-aware OCI compute provisioner that handles temporary host-capacity shortages with bounded retries, idempotency, cost guardrails, and duplicate-instance prevention.
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.
| 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. |
| 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.
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-provisionerOther 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 versionoci-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 existsTry it without an Oracle Cloud account first:
make install && make demoThe demo runs the whole flow — including a capacity shortage in two availability domains — against an in-process fake. No credentials, no network, nothing created.
The tool uses OCI API key authentication and reads ~/.oci/config. It never
reads, copies, or logs your private 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- Sign in to the OCI Console.
- Open the profile menu (top right) and choose My profile.
- Under Resources, select API keys.
- Click Add API key, choose Paste a public key, and paste the contents
of
~/.oci/oci_api_key_public.pem. - Click Add. The Console shows a configuration file preview containing
your
user,fingerprint,tenancy, andregion. Copy it.
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.pemchmod 600 ~/.oci/configThe 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 -coci-capacity-provisioner doctorOracle's full walkthrough: Required Keys and OCIDs.
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.
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.pubIt 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.
oci-capacity-provisioner doctor
oci-capacity-provisioner doctor --offline # no network at allChecks 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.
oci-capacity-provisioner validate-config # static, no network
oci-capacity-provisioner validate-config --online # also verifies OCI resourcesoci-capacity-provisioner launch --dry-runResolves 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.
oci-capacity-provisioner launch --onceTries 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".
oci-capacity-provisioner launchRuns 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.
oci-capacity-provisioner statusAsks OCI directly for the instance's lifecycle state, availability domain, shape, resources, masked OCID, addresses, and creation time.
oci-capacity-provisioner version| 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 |
See config.example.toml for a fully annotated file, and
examples/openclaw.toml for a worked example.
| 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 |
| 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 |
| 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 |
| 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 |
| Key | Default | Meaning |
|---|---|---|
level |
"INFO" |
DEBUG, INFO, WARNING, ERROR |
json_log_path |
"./logs/provisioner.jsonl" |
"" disables the audit trail |
| 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.
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.
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.Flexonly. 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.
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 = 1andmemory_gb = 6inconfig.toml. -
If you already have an A1 instance using the allowance,
launchwill 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 isSTOPPED— a stopped instance still holds its allocation.doctorshows 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
ocpusandmemory_gbso the total fits. -
If your user cannot list every compartment in the tenancy,
doctorreportsallowance.scope: WARNand says so. The totals may then undercount, because an A1 instance in a compartment you cannot see still consumes the allowance.
- 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.
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.
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.
| 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.
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):
- 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
TERMINATEDcounts, includingSTOPPED. If the query itself fails, no request is sent. - 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. - 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.
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.
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.42If 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.
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 launchLinux with systemd — a user service that restarts on failure:
systemctl --user start oci-a1-provisioner
journalctl --user -u oci-a1-provisioner -fAnywhere — tmux or screen:
tmux new -s a1 'oci-capacity-provisioner launch'
tmux attach -t a1nohup ... & works too, but nothing then delivers SIGINT for a clean
shutdown; send SIGTERM instead of killing the process.
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.
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
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.
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
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:
- The compartment OCID is correct and your user has a policy for it.
- The subnet is in the same region as
[oci].region— subnet OCIDs are region-specific. - 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 doctorI 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— pointssh_public_key_pathat the.pubfile. 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 statusdoctor 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.
- Credentials are read from
~/.oci/configby the OCI SDK. This tool never copies, stores, or logs private key material. - Generated files are written with
0600permissions. - 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.
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
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.