One tool to fetch everything about any Oracle Cloud resource, just by pasting an OCID
If you work with Oracle Cloud Infrastructure (OCI) regularly, you know the drill. You get an OCID from a colleague, a ticket, or a monitoring alert — and now you need to find out what that resource actually is, what region it lives in, how much CPU it has, which compartment it belongs to, what version the database is running. You end up clicking through four different screens in the OCI Console to piece it together.
I got tired of that. So I built OCI Inspector — a Python web application that lets you paste any OCID and instantly get every detail about that resource in one place. No console navigation. No region switching. No manual cross-referencing.
This post walks through the full project — the architecture, how each piece works, and how the compartment scanning feature crawls all your subscribed regions in parallel.
What OCI Inspector Does
The core idea is simple:
Paste an OCID → get A to Z details about that resource
But it goes further than that:
- Multi-tenancy — You register multiple OCI tenancies (credentials stored in PostgreSQL), and you can query resources across all of them from one interface
- Auto-region detection — It reads the region code embedded in the OCID itself, so cross-region lookups just work automatically
- Compartment scanning — Pick any compartment and scan every subscribed region in parallel, listing all Compute instances, DB Systems, and Autonomous Databases in one table
- Authentication — Login system with username/password, admin and user roles, user management
- Query history — Keeps track of your recent lookups
Supported resource types:
- 🖥️ Compute Instances
- 🗄️ DB Systems (DBCS)
- ⚡ Autonomous Databases
Project Structure
oci-inspector/
├── app.py # Flask app factory + admin user seeding
├── wsgi.py # Gunicorn entrypoint
├── requirements.txt
├── models/
│ └── models.py # SQLAlchemy: User, Tenancy, QueryHistory
├── routes/
│ ├── auth_routes.py # Login, logout, user management
│ ├── tenancy_routes.py # CRUD for OCI tenancy credentials
│ ├── inspect_routes.py # Single OCID lookup API
│ └── scan_routes.py # Compartment scan API
├── utils/
│ └── oci_helper.py # All OCI SDK logic lives here
└── templates/
├── base.html # Dark dashboard layout, sidebar nav
├── auth/ # Login, profile, user management pages
├── inspect/ # Resource lookup UI
├── tenancies/ # Tenancy add/edit forms
└── scan/ # Compartment scanner UIThe stack is intentionally lean:
- Flask — web framework
- SQLAlchemy + Flask-Migrate — ORM and database migrations
- Flask-Login — session management and authentication
- OCI Python SDK — official Oracle Cloud SDK
- PostgreSQL — credential storage
- Bootstrap 5 — frontend (dark themed dashboard)
The Core Insight: Reading the Region From the OCID
Every OCI resource has an OCID (Oracle Cloud Identifier). The format is:
ocid1.<resource_type>.<realm>.<region_key>.<unique_id>For example:
ocid1.dbsystem.oc1.iad.anuwcljsbdze2maazh7...
ocid1.instance.oc1.hyd.abcdefgh...
ocid1.autonomousdatabase.oc1.bom.xyz123...Two things you can extract from this automatically:
1. The resource type — segment 2 (dbsystem, instance, autonomousdatabase) tells you exactly what kind of resource it is. No guessing.
2. The region — segment 4 is a short region code (iad = us-ashburn-1, hyd = ap-hyderabad-1, bom = ap-mumbai-1). This is the key to making cross-region lookups work.
Here's the detection logic in utils/oci_helper.py:
def detect_resource_type(ocid: str) -> str:
parts = ocid.split(".")
resource_type = parts[1].lower()
type_map = {
"instance": "compute",
"dbsystem": "dbsystem",
"autonomousdatabase": "autonomous_database",
}
return type_map.get(resource_type, resource_type)
def extract_region_from_ocid(ocid: str) -> str | None:
parts = ocid.split(".")
region_segment = parts[3].lower()
# Already a full region name like "ap-hyderabad-1"?
if "-" in region_segment:
return region_segment
# Map short code to full region name
return OCID_REGION_MAP.get(region_segment)The OCID_REGION_MAP is a dictionary covering all OCI regions:
OCID_REGION_MAP = {
"iad": "us-ashburn-1",
"hyd": "ap-hyderabad-1",
"bom": "ap-mumbai-1",
"fra": "eu-frankfurt-1",
"lhr": "eu-london-1",
"nrt": "ap-tokyo-1",
# ... all regions
}This matters enormously. OCI returns a 404 error if you query a resource through the wrong region's API endpoint — it doesn't helpfully redirect you. Before this was implemented, a user configured with ap-hyderabad-1 as their home region would get a confusing 404 every time they tried to look up a resource that actually lived in us-ashburn-1. Now the SDK client is always pointed at the right endpoint automatically.
The Signer: Connecting to OCI With Stored Credentials
OCI's API uses request signing — every HTTP request is signed with your RSA private key. The application stores the PEM key content in the database and builds a signer at request time.
One critical gotcha with oci.signer.Signer: private_key_file_location is a required positional argument even when you're supplying key content directly. Passing it as None raises a TypeError. The correct pattern:
from oci.signer import Signer
def build_signer(tenancy, ocid_region=None):
config = {
"tenancy": tenancy.tenancy_ocid,
"user": tenancy.user_ocid,
"fingerprint": tenancy.fingerprint,
"region": ocid_region or tenancy.region, # override region from OCID
"key_content": tenancy.pem_key,
}
signer = Signer(
tenancy=config["tenancy"],
user=config["user"],
fingerprint=config["fingerprint"],
private_key_file_location="", # required positional — pass empty string
pass_phrase=None,
private_key_content=config["key_content"], # SDK handles PEM loading internally
)
return config, signerThe OCI SDK internally calls its own load_private_key() when private_key_content is provided, so you just pass the raw PEM string from the database and it works.
Fetching Resource Details
Once you have a config and signer, each resource type has its own fetch function. Here's a simplified version of the Compute instance fetcher to show the pattern:
def fetch_compute_details(tenancy, ocid: str) -> dict:
ocid_region = extract_region_from_ocid(ocid)
config, signer = build_signer(tenancy, ocid_region=ocid_region)
compute_client = oci.core.ComputeClient(config, signer=signer)
identity_client = oci.identity.IdentityClient(config, signer=signer)
network_client = oci.core.VirtualNetworkClient(config, signer=signer)
instance = compute_client.get_instance(ocid).data
# Shape config: OCPUs, RAM, network bandwidth
shape_config = {}
if instance.shape_config:
sc = instance.shape_config
shape_config = {
"ocpus": sc.ocpus,
"memory_in_gbs": sc.memory_in_gbs,
"networking_bandwidth_in_gbps": sc.networking_bandwidth_in_gbps,
"processor_description": sc.processor_description,
}
# Boot volume size
boot_volumes = compute_client.list_boot_volume_attachments(
availability_domain=instance.availability_domain,
compartment_id=instance.compartment_id,
instance_id=ocid,
).data
# VNICs and IPs
vnic_attachments = compute_client.list_vnic_attachments(
compartment_id=instance.compartment_id,
instance_id=ocid
).data
vnics = []
for va in vnic_attachments:
vnic = network_client.get_vnic(va.vnic_id).data
vnics.append({
"private_ip": vnic.private_ip,
"public_ip": vnic.public_ip,
"is_primary": vnic.is_primary,
})
return {
"resource_type": "Compute Instance",
"display_name": instance.display_name,
"lifecycle_state": instance.lifecycle_state,
"shape": instance.shape,
"shape_config": shape_config,
"vnics": vnics,
# ... more fields
}The DB System fetcher additionally drills into DB Homes to get individual database details — name, version, workload type, character set. The Autonomous Database fetcher pulls connection strings, backup config, maintenance windows, and auto-scaling settings.
The main dispatcher ties it all together:
def fetch_resource(tenancy, ocid: str) -> dict:
resource_type = detect_resource_type(ocid)
handlers = {
"compute": fetch_compute_details,
"dbsystem": fetch_dbsystem_details,
"autonomous_database": fetch_autonomous_db_details,
}
handler = handlers.get(resource_type)
if not handler:
return {"error": True, "message": f"Unsupported type: {resource_type}"}
return handler(tenancy, ocid)Compartment Scanning: All Regions in Parallel
The compartment scan feature is where things get interesting. The goal: given a compartment OCID, find every Compute instance, DB System, and Autonomous Database in that compartment — across all subscribed regions.
OCI resources are regional. A compartment can contain resources in any region your tenancy is subscribed to. To get a complete picture you have to query every region separately. With some tenancies having 20+ subscribed regions, doing this sequentially could take minutes.
The solution is concurrent.futures.ThreadPoolExecutor:
def scan_compartment_all_regions(tenancy, compartment_id: str) -> dict:
import concurrent.futures
# Get all regions this tenancy is subscribed to
subscribed = fetch_subscribed_regions(tenancy)
region_names = [r["region_name"] for r in subscribed]
all_compute, all_dbsystems, all_adb, all_errors = [], [], [], []
def scan_one(region):
return _scan_region_for_compartment(tenancy, compartment_id, region)
# Scan all regions in parallel — 8 threads
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
futures = {executor.submit(scan_one, r): r for r in region_names}
for future in concurrent.futures.as_completed(futures):
result = future.result()
all_compute.extend(result["compute"])
all_dbsystems.extend(result["dbsystems"])
all_adb.extend(result["autonomous_databases"])
all_errors.extend(result["errors"])
return {
"regions_scanned": sorted(region_names),
"total_resources": len(all_compute) + len(all_dbsystems) + len(all_adb),
"compute": sorted(all_compute, key=lambda x: x["display_name"]),
"dbsystems": sorted(all_dbsystems, key=lambda x: x["display_name"]),
"autonomous_databases": sorted(all_adb, key=lambda x: x["display_name"]),
"errors": all_errors,
}Each _scan_region_for_compartment() call spins up three separate OCI SDK list calls for that region — list_instances, list_db_systems, list_autonomous_databases — each wrapped in a try/except so one failed region doesn't abort the whole scan. Errors are collected and shown as warnings in the UI without blocking the results.
For fetching the subscribed regions list itself:
def fetch_subscribed_regions(tenancy) -> list:
config, signer = build_signer(tenancy)
identity_client = oci.identity.IdentityClient(config, signer=signer)
regions = identity_client.list_region_subscriptions(
tenancy_id=tenancy.tenancy_ocid
).data
return [
{
"region_name": r.region_name,
"is_home_region": r.is_home_region,
}
for r in regions if r.status == "READY"
]And for compartments, the key is compartment_id_in_subtree=True which recursively lists all nested sub-compartments in one call:
def fetch_compartments(tenancy) -> list:
config, signer = build_signer(tenancy)
identity_client = oci.identity.IdentityClient(config, signer=signer)
all_compartments = oci.pagination.list_call_get_all_results(
identity_client.list_compartments,
compartment_id=tenancy.tenancy_ocid,
compartment_id_in_subtree=True, # gets all nested compartments
access_level="ACCESSIBLE",
).data
return [
{"id": c.id, "name": c.name, "parent": c.compartment_id}
for c in all_compartments
if c.lifecycle_state == "ACTIVE"
]Authentication
Authentication uses Flask-Login with Werkzeug password hashing. The User model:
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(256), nullable=False)
is_admin = db.Column(db.Boolean, default=False)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)On first boot, if no users exist in the database, a default admin account is created automatically from environment variables:
def _seed_admin():
if User.query.count() == 0:
admin = User(
username=os.environ.get("ADMIN_USERNAME", "admin"),
is_admin=True
)
admin.set_password(os.environ.get("ADMIN_PASSWORD", "changeme123"))
db.session.add(admin)
db.session.commit()Every route is protected with @login_required. Admin-only routes (user management) check current_user.is_admin explicitly.
The Data Model
Three tables in PostgreSQL:
users — Application login accounts. Stores bcrypt-hashed passwords, admin flag, last login timestamp.
tenancies — OCI credentials per tenancy. Stores tenancy OCID, user OCID, fingerprint, home region, and the PEM private key content as text.
query_history — A log of every lookup and scan performed. Records the OCID queried, detected resource type, display name, scan type (ocid or compartment), and which user ran it.
UI: How the Frontend Works
The UI is server-rendered Jinja2 templates with Bootstrap 5 for layout and vanilla JavaScript for the dynamic parts. No React, no build step.
The Resource Lookup page works entirely client-side after the initial load: you select a tenancy from a dropdown, paste an OCID into a textarea, and click Inspect. JavaScript fetch() calls the /inspect/query endpoint, gets back a JSON payload, and renders the result inline — grouped by section (Identity, Compute, Network, Storage, Tags), with OCID values shown as clickable copy-to-clipboard pills.
The Compartment Scan page is a four-step wizard: select tenancy → select compartment (dynamically loaded via /scan/api/compartments/<id>) → configure scan mode → results. While the scan is running, a loading spinner cycles through the region names being queried. Results come back as a tabbed table: one tab each for Compute, DB Systems, and Autonomous Databases. Regions that had resources are highlighted in green; empty regions in grey.
Lessons Learned Building This
The 404 trap. OCI's API returns 404 Authorization failed or requested resource not found for both actual missing resources and for querying the wrong regional endpoint. If you initialize an SDK client with ap-hyderabad-1 and try to get a resource that lives in us-ashburn-1, you get a 404 with no hint about the real cause. Parsing the region from the OCID and overriding the client config eliminates this entirely.
private_key_file_location is always required. Even when you're using private_key_content. The oci.signer.Signer class signature requires it as a positional argument. Pass an empty string "" when supplying key content — it gets ignored, but it must be present.
Parallel scanning is essential. A tenancy with 20 subscribed regions scanning 3 resource types each = 60 API calls. Sequential: ~60 seconds. With 8 threads: ~8–12 seconds. For a web app where users are watching a loading spinner, that difference matters enormously.
compartment_id_in_subtree=True is the magic parameter. Without it, list_compartments only returns direct children. With it, you get the entire hierarchy recursively in one call.
Extending the Project
The architecture makes it easy to add new resource types. The pattern is always the same:
- Add the OCID prefix to
type_mapindetect_resource_type() - Add the short region code to
OCID_REGION_MAPif needed - Write a
fetch_<type>_details(tenancy, ocid)function - Register it in the
handlersdict insidefetch_resource() - Add list logic inside
_scan_region_for_compartment()for compartment scanning - Add a rendering section in the frontend JS
Natural next additions would be Load Balancers, VCNs, Object Storage buckets, Exadata infrastructure, and OKE clusters — all of which have stable OCI SDK support.
Wrapping Up
OCI Inspector started as a personal productivity tool and turned into a fairly complete internal platform for OCI resource management. The three key technical pieces that make it work well are:
- OCID parsing — extracting resource type and region from the identifier itself, eliminating manual configuration and cross-region 404s
- Parallel region scanning — using
ThreadPoolExecutorto query all regions simultaneously instead of sequentially - Credential-per-tenancy model — storing full OCI API credentials in the database so a single app instance can manage multiple separate OCI tenancies
The full project — routes, models, templates, OCI helper utilities — is around 800 lines of Python and 1,200 lines of HTML/JS. It's a practical example of how the OCI Python SDK can be wrapped into a usable web interface without a lot of complexity.
If you're working with OCI and find yourself doing a lot of manual console navigation to track down resource details, something like this can save a significant amount of time.
Built with Python, Flask, OCI Python SDK, SQLAlchemy, and Bootstrap 5.