8 Commits

Author SHA1 Message Date
sku-refresh-bot
32319b67d7 chore(pricing): monthly SKU catalog refresh (2026-04-24)
Automated refresh from Oracle public pricing API.
See workflow artifact 'sku-refresh-logs' for the diff.
2026-04-24 15:53:41 +00:00
root
9fbe05a6a5 Use per-run branch name to avoid push collisions
All checks were successful
Deploy Skill to OCI / deploy (push) Successful in 19s
Prior run pushed automation/sku-refresh-2026-04-24 successfully but
failed before opening the PR (issue-creation bug, now fixed). On the
next run, main has moved, so a new commit on the same branch name has
a different parent → non-fast-forward rejection.

Append ${{ github.run_number }} to make the branch unique per workflow
invocation. Orphan branches left behind can be cleaned up after their
PRs are merged/closed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 12:45:57 -03:00
root
9b4d04cf34 Make discover count machine-readable for CI parsing
All checks were successful
Deploy Skill to OCI / deploy (push) Successful in 25s
The workflow's grep-on-free-text approach was fragile: local run correctly
extracted 162, but in the CI run the 'Open issue' step was skipped, meaning
new_count ended up as 0 after parsing. Likely causes: stdout buffering,
color codes, or shell differences between local and the Gitea runner's
shell.

Fix: tools/refresh_sku_catalog.py --discover now emits a deterministic
last-line marker `DISCOVER_MISSING_COUNT=<n>`. The workflow parses that
with sed (anchored, unambiguous) instead of the human-facing summary
line. Also added a diagnostic echo so the parsed value shows up in CI
logs for future debugging.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 12:43:06 -03:00
root
30394ece48 Pin upload-artifact to v3 for Gitea Actions compatibility
All checks were successful
Deploy Skill to OCI / deploy (push) Successful in 21s
Gitea Actions runner does not support actions/upload-artifact v2.0.0+
(error: "@actions/artifact v2.0.0+, upload-artifact@v4+ and
download-artifact@v4+ are not currently supported on GHES").

The v4 release switched to a new artifact API that only GitHub-hosted
runners implement. v3 still uses the v1 API and works on Gitea /
self-hosted / GHES runners.

Downgraded in both sku-catalog-refresh.yaml and kb-health.yaml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 12:31:58 -03:00
root
480c4bca52 Fix Python setup in Gitea Actions workflows
All checks were successful
Deploy Skill to OCI / deploy (push) Successful in 25s
The oci-mcp-runner uses node:20-bookworm which runs as root and does
NOT ship with sudo installed. Our workflows used 'sudo apt-get' → step
failed with 'sudo: command not found' (exit 127) on first invocation
of sku-catalog-refresh.yaml.

Fix both affected workflows (sku-catalog-refresh + kb-health):
- Drop the `sudo` prefix (we're already root).
- Install Python deps via apt packages (python3-requests, python3-yaml,
  python3-bs4) instead of pip. Avoids PEP 668 / externally-managed
  environment error on Debian 12 and is faster/cacheable by the runner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 12:24:35 -03:00
root
433b08071b Use auto-provisioned GITHUB_TOKEN in SKU refresh workflow
All checks were successful
Deploy Skill to OCI / deploy (push) Successful in 30s
Switch from manually-configured secrets.GITEA_TOKEN to the auto-provisioned
secrets.GITHUB_TOKEN that Gitea Actions creates per run. Scoped to the repo,
rotates per workflow run, no manual config needed.

Added explicit permissions block (contents:write, pull-requests:write,
issues:write) so the ephemeral token has the scopes required to push a
branch, open the PR, and open the issue for new SKUs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:51:00 -03:00
root
874d40d38a Improve SKU catalog hygiene and BOM accuracy
All checks were successful
Deploy Skill to OCI / deploy (push) Successful in 21s
- refresh_sku_catalog.py: add --discover to report API SKUs missing from
  catalog, filtered to already-curated serviceCategory values. Auto-runs
  at end of --validate and --refresh so gaps surface on every maintenance
  pass. First run found 162 missing SKUs (Blackwell GPUs, X12 Ax compute,
  VMware reserved tiers, WebLogic-on-OKE, Analytics/OIC BYOL).
- oci_bom_gen.py: fix Cost % column being blank. Back-fill was gated on
  formula detection but data rows write raw numbers, so the predicate
  never matched. Now tracks data_item_rows explicitly.
- .gitea/workflows/sku-catalog-refresh.yaml: monthly automation (1st at
  09:00 UTC). Auto-refresh prices → push branch + open PR; detect new
  SKUs → open issue with labels. Gated on secrets.GITEA_TOKEN.
- kb/pricing/oci-sku-catalog.yaml: add B95714 / B95715 (Autonomous ATP
  Dedicated ECPU, LI + BYOL) — canonical SKUs for ADB-D pricing.
- kb/services/compute.yaml: add X12 family (VM.Standard4.Ax.Flex,
  BM.Standard4.Ax.120), E6.Ax and A4.Ax variants. Verified against
  docs.oracle.com computeshapes.htm. Bump last_verified + changelog.
- templates/bom-spec.yaml: document 730 hrs/month convention as default
  (= real annual billing / 12; 744 overstates by 1.92% on 12-month TCO).
- Makefile: new 'make sku-discover' target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:40:53 -03:00
root
a22711c065 Update ExaCS X11M KB with authoritative datasheet figures
All checks were successful
Deploy Skill to OCI / deploy (push) Successful in 30s
Source: ADB-D X11M Datasheet v1.0 (Jan 2025), Table 1.

- Add HC storage variant (default): 80 TB usable disk/SS, 264 TB raw, 1.25 TB
  XRMEM + 27.2 TB flash cache per SS. Keep EF variant (52.5 TB usable/SS).
- Add x11m_elastic_base (2 DB + 3 SS): 240 TB total usable disk, 192 TB Max
  DB Size no-backup (what OCI Cost Estimator caps at), 96 TB with backup.
  Include performance metrics (5.6M read IOPS, 300 GB/s flash bw, etc.).
- Add x11m_base_system as distinct SKU: fixed-spec, non-expandable, 192
  ECPUs / 73 TB usable disk / 10 GbE — not the same as elastic 2+3.
- Replace misleading rack_configurations table (mixed X9M figures) with
  elastic per-server model; 'quarter/half/full rack' noted as legacy.
- Add pricing verified against OCI Cost Estimator 2026-04-23: DB and SS at
  \$2.9032/hr each; base config \$10,800/month excluding licenses.
- Add gotcha exacs_x11m_hc_vs_ef: default quotes to EF without
  justification; HC serves hot data from flash cache at same latency and
  far better \$/TB.
- Document the three distinct storage figures (Total Usable Disk vs Max DB
  Size no-backup vs with-backup) to avoid customer confusion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 10:02:42 -03:00
9 changed files with 440 additions and 53 deletions

View File

@@ -19,13 +19,10 @@ jobs:
- name: Setup Python
run: |
sudo apt-get update -qq
sudo apt-get install -y python3 python3-pip
apt-get update -qq
apt-get install -y --no-install-recommends python3 python3-requests python3-bs4 python3-yaml
python3 --version
- name: Install dependencies
run: python3 -m pip install --user requests beautifulsoup4 pyyaml
- name: Check Architecture Center links
id: check
continue-on-error: true
@@ -71,7 +68,7 @@ jobs:
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: kb-health-report
path: /tmp/link-check.txt

View File

@@ -0,0 +1,147 @@
# Monthly SKU catalog refresh.
#
# Runs on the 1st of each month at 09:00 UTC and on manual dispatch.
#
# Does two things:
# 1. `--refresh`: pulls latest PAYG prices from Oracle's public API,
# updates kb/pricing/oci-sku-catalog.yaml, and opens a PR if anything
# changed.
# 2. `--discover`: reports SKUs present in the API but absent from the
# catalog (filtered to serviceCategory values already curated).
# If new SKUs are found, opens a Gitea issue listing them by category
# for manual review.
#
# Token:
# Uses the auto-provisioned secrets.GITHUB_TOKEN (Gitea Actions provides this
# per-run, scoped to this repo, GitHub-Actions-compatible). No manual secret
# configuration required. The explicit `permissions:` block below grants the
# scopes needed to push a branch, open a PR, and open an issue.
name: SKU Catalog Refresh
on:
schedule:
- cron: '0 9 1 * *'
workflow_dispatch:
permissions:
contents: write
pull-requests: write
issues: write
jobs:
refresh-and-discover:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Python
run: |
apt-get update -qq
apt-get install -y --no-install-recommends python3 python3-requests python3-yaml jq
python3 --version
- name: Refresh prices from Oracle API
id: refresh
run: |
python3 tools/refresh_sku_catalog.py --refresh --diff 2>&1 \
| tee /tmp/refresh.log
if git diff --quiet kb/pricing/oci-sku-catalog.yaml; then
echo "changed=false" >> $GITHUB_OUTPUT
else
echo "changed=true" >> $GITHUB_OUTPUT
fi
- name: Discover missing SKUs
id: discover
run: |
python3 tools/refresh_sku_catalog.py --discover -v 2>&1 \
| tee /tmp/discover.log
NEW_COUNT=$(sed -n 's/^DISCOVER_MISSING_COUNT=\([0-9]\+\)$/\1/p' \
/tmp/discover.log | tail -1)
echo "Parsed new_count='${NEW_COUNT}'"
echo "new_count=${NEW_COUNT:-0}" >> $GITHUB_OUTPUT
- name: Upload logs as artifact
if: always()
uses: actions/upload-artifact@v3
with:
name: sku-refresh-logs
path: |
/tmp/refresh.log
/tmp/discover.log
retention-days: 90
- name: Open PR with price updates
if: steps.refresh.outputs.changed == 'true'
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITEA_SERVER: ${{ github.server_url }}
GITEA_REPO: ${{ github.repository }}
run: |
DATE=$(date -u +%Y-%m-%d)
BRANCH="automation/sku-refresh-${DATE}-run${{ github.run_number }}"
git config user.name "sku-refresh-bot"
git config user.email "sku-refresh@automation.local"
git checkout -b "$BRANCH"
git add kb/pricing/oci-sku-catalog.yaml
git commit -m "chore(pricing): monthly SKU catalog refresh (${DATE})
Automated refresh from Oracle public pricing API.
See workflow artifact 'sku-refresh-logs' for the diff."
# Push using token auth
REMOTE=$(echo "$GITEA_SERVER/$GITEA_REPO.git" \
| sed "s#https://#https://sku-refresh-bot:${GITEA_TOKEN}@#")
git push "$REMOTE" "$BRANCH"
# Open PR via Gitea API
OWNER=$(echo "$GITEA_REPO" | cut -d/ -f1)
REPO=$(echo "$GITEA_REPO" | cut -d/ -f2)
curl -sS -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${GITEA_SERVER}/api/v1/repos/${OWNER}/${REPO}/pulls" \
-d "$(jq -n \
--arg title "chore(pricing): monthly SKU refresh ${DATE}" \
--arg head "$BRANCH" \
--arg base "main" \
--arg body "$(printf 'Automated refresh — [refresh log artifact](../../actions).\n\n```\n%s\n```' \
"$(tail -40 /tmp/refresh.log)")" \
'{title:$title, head:$head, base:$base, body:$body}')"
- name: Open issue for new SKUs
if: steps.discover.outputs.new_count != '0'
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITEA_SERVER: ${{ github.server_url }}
GITEA_REPO: ${{ github.repository }}
NEW_COUNT: ${{ steps.discover.outputs.new_count }}
run: |
DATE=$(date -u +%Y-%m-%d)
OWNER=$(echo "$GITEA_REPO" | cut -d/ -f1)
REPO=$(echo "$GITEA_REPO" | cut -d/ -f2)
BODY=$(printf '## %s new SKUs detected in Oracle API\n\n**Detected:** %s\n\nThese SKUs are present in the Oracle public pricing API but not yet in `kb/pricing/oci-sku-catalog.yaml`. Filter: serviceCategory values already represented in our catalog (so only relevant families show up).\n\n### Full listing\n\n```\n%s\n```\n\n### Next step\n\nReview and add relevant entries to `kb/pricing/oci-sku-catalog.yaml` under the appropriate category block. After merging, prices will be picked up automatically on the next monthly refresh.\n' \
"$NEW_COUNT" "$DATE" "$(cat /tmp/discover.log)")
curl -sS -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${GITEA_SERVER}/api/v1/repos/${OWNER}/${REPO}/issues" \
-d "$(jq -n \
--arg title "SKU catalog: ${NEW_COUNT} new SKUs to review (${DATE})" \
--arg body "$BODY" \
'{title:$title, body:$body, labels:["automation","sku-catalog"]}')"
- name: Summary
if: always()
run: |
echo "## SKU Refresh Summary" >> $GITHUB_STEP_SUMMARY
echo "- Catalog changed: ${{ steps.refresh.outputs.changed }}" >> $GITHUB_STEP_SUMMARY
echo "- New SKUs discovered: ${{ steps.discover.outputs.new_count }}" >> $GITHUB_STEP_SUMMARY

View File

@@ -1,6 +1,6 @@
# OCI Deal Accelerator — Build Automation
.PHONY: help install test validate example diagram deck full clean lint codex-package update-icons freshness freshness-refresh sync-skill
.PHONY: help install test validate example diagram deck full clean lint codex-package update-icons freshness freshness-refresh sync-skill sku-discover
# Use venv if present, otherwise find best available python3
ifneq (,$(wildcard .venv/bin/python))
@@ -97,3 +97,6 @@ kb-check: ## KB freshness JSON (used by skill welcome-flow pre-flight)
freshness-refresh: ## Run refresh tools for stale KB files that support automation
@$(PYTHON) tools/kb_freshness.py --auto-refresh
sku-discover: ## Report SKUs present in Oracle API but missing from oci-sku-catalog.yaml
@$(PYTHON) tools/refresh_sku_catalog.py --discover

View File

@@ -1,4 +1,4 @@
last_verified: '2026-03-27'
last_verified: '2026-04-24'
source: Oracle OCI Price List + internal BOM reference v1.73
description: 'OCI SKU catalog for BOM generation. ~160 SKUs across 14+ categories. Prices are approximate and for estimation
only. Always verify against https://www.oracle.com/cloud/pricing/
@@ -560,6 +560,18 @@ categories:
list_price_usd: 0.0807
default_hours_units: 730
billing_type: hourly
- sku: B95714
product: Oracle Autonomous AI Transaction Processing - Dedicated - ECPU
metric: ECPU Per Hour
list_price_usd: 0.0807
default_hours_units: 730
billing_type: hourly
- sku: B95715
product: Oracle Autonomous AI Transaction Processing - Dedicated - ECPU - BYOL
metric: ECPU Per Hour
list_price_usd: 0.0807
default_hours_units: 730
billing_type: hourly
exascale:
display_name: Oracle Cloud Infrastructure - Database Exadata Exascale Infrastructure
skus:
@@ -1289,12 +1301,6 @@ categories:
list_price_usd: 0.015
default_hours_units: 730
billing_type: hourly
# OCI Data Science and Data Flow do not have dedicated SKUs in the Oracle
# public pricing API — they are billed via the underlying Compute + Block/Object
# Storage consumed by notebook sessions, model-deployment endpoints, and Spark
# runs. The entries below are placeholder estimates (VM.Standard3.Flex OCPU rate)
# so BOMs can include them as line items; always confirm against the customer's
# actual shape + usage before quoting.
- sku: EST-DS-NOTEBOOK
product: '[ESTIMATE] OCI Data Science - Notebook Session (billed via underlying VM.Standard compute OCPU)'
metric: OCPU Per Hour

View File

@@ -1,8 +1,11 @@
---
last_verified: 2026-03-14
last_verified: 2026-04-24
service: OCI Compute
category: compute
oci_color: "#4F7B6E"
changelog:
- date: "2026-04-24"
change: "Added missing shapes surfaced by discover+WebFetch: VM/BM.Standard.E6.Ax, VM/BM.Standard.A4.Ax, and full X12 family (VM.Standard4.Ax.Flex 39 OCPU / 360 GB, BM.Standard4.Ax.120 latest-gen bare metal). Verified against docs.oracle.com/en-us/iaas/Content/Compute/References/computeshapes.htm."
---
what: Virtual machines and bare metal instances. Flexible shapes allow custom
@@ -42,6 +45,24 @@ shape_families:
max_ocpu: 18
memory_per_ocpu_gb: "1-64 GB per OCPU"
use_for: "High-frequency trading, HPC"
- name: VM.Standard.E6.Ax.Flex
processor: "AMD EPYC (Ampere-branded E6 variant)"
max_ocpu: 94
max_memory_gb: 712
network_gbps: 99
use_for: "E6 Ampere-variant general purpose"
- name: VM.Standard.A4.Ax.Flex
processor: "Arm (A4 Ampere-branded variant)"
max_ocpu: 45
max_memory_gb: 720
network_gbps: 100
use_for: "A4 Ampere-variant Arm workloads"
- name: VM.Standard4.Ax.Flex
processor: "AMD EPYC (X12 Ampere-branded)"
max_ocpu: 39
max_memory_gb: 360
network_gbps: 99
use_for: "X12-generation general purpose (latest)"
- name: BM.Standard.E5
processor: "AMD EPYC 9J14 (Genoa)"
ocpus: 192
@@ -50,6 +71,30 @@ shape_families:
processor: "AMD EPYC 9J45 (Turin)"
ocpus: 256
use_for: "Bare metal latest generation"
- name: BM.Standard.E6.Ax.192
processor: "AMD EPYC (E6 Ampere-branded variant)"
ocpus: 192
max_memory_gb: 1536
network_gbps: 200
use_for: "Bare metal E6 Ampere-variant"
- name: BM.Standard.A4.48
processor: "Arm (A4)"
ocpus: 48
max_memory_gb: 768
network_gbps: 100
use_for: "Bare metal A4 Arm general purpose"
- name: BM.Standard.A4.Ax.48
processor: "Arm (A4 Ampere-branded variant)"
ocpus: 48
max_memory_gb: 768
network_gbps: 100
use_for: "Bare metal A4 Ampere-variant Arm"
- name: BM.Standard4.Ax.120
processor: "AMD EPYC (X12 Ampere-branded)"
ocpus: 120
max_memory_gb: 1152
network_gbps: 200
use_for: "Bare metal X12 latest generation"
- name: VM.GPU.A10.1
processor: "NVIDIA A10"
gpu_count: 1

View File

@@ -1,5 +1,5 @@
# OCI Service: Exadata Cloud Service (ExaCS)
# Last verified: 2026-03
# Last verified: 2026-04
service:
name: "Exadata Cloud Service (ExaCS)"
@@ -35,37 +35,102 @@ variants:
usable_tb: 63.6
- id: exacs_x11m
name: "ExaCS X11M"
description: "Exadata X11M shape — latest generation with AMD EPYC 9J25 processors and XRMEM acceleration."
description: "Exadata X11M shape — latest generation with AMD EPYC processors and XRMEM acceleration. Two storage server variants: HC (default, capacity-optimised) and EF (Extreme Flash, latency-optimised)."
per_db_server:
ecpus: 760
memory_gb: 1390
xrmem_gb: 1250
flash_cache_tb: 27.2
per_storage_server:
usable_tb: 52.5
cpu: "2 × AMD EPYC 9J25 (96 cores each, 192 cores per server)"
storage_server_variants:
hc:
name: "X11M High Capacity (HC)"
description: "Default storage variant. 12 × 22 TB SAS HDDs + 27.2 TB flash cache + 1.25 TB XRMEM per SS. Best $/TB; same hot-data latency as EF (served from flash cache + XRMEM)."
raw_tb_per_ss: 264
flash_cache_tb_per_ss: 27.2
xrmem_tb_per_ss: 1.25
usable_disk_tb_per_ss: 80 # Datasheet "Total Usable Disk Capacity" per SS — post HIGH redundancy + drive-failure reserve, before compression
max_db_size_tb_per_ss_no_local_backup: 64 # Derived: 192 TB / 3 SS at base config (datasheet Elastic Example 1)
max_db_size_tb_per_ss_with_local_backup: 32 # Derived: 96 TB / 3 SS at base config
total_cores_per_ss: 64
recommended_for: "OLTP, mixed workloads, DWH where active set fits in flash cache (~80% of cases)"
ef:
name: "X11M Extreme Flash (EF)"
description: "All-flash storage variant. Lower latency on cache-miss reads, but higher $/TB. Use when the working set exceeds flash cache or for latency-sensitive DWH full scans."
usable_tb_per_ss: 52.5
type: "Extreme Flash (EF)"
recommended_for: "Latency-sensitive DWH with large full-table scans that miss flash cache; sub-ms consistent latency requirements"
pricing_2026_04:
source: "OCI Cost Estimator 2026-04-23"
db_server_per_hour_usd: 2.9032
storage_server_per_hour_usd: 2.9032
base_config_monthly_usd: 10800 # (2 DB + 3 SS) × $2.9032 × 744 hr; excludes DB licenses (BYOL) and ECPU charges
sizing:
model: "Elastic per-server (2-32 DB servers, 3-64 storage servers)"
rack_configurations:
quarter_rack:
ocpus: "252 (X9M: 2 × 126/server)"
memory: "2.71 TB (X9M: 2 × 1,390 GB)"
storage: "Up to 190.8 TB usable (X9M: 3 SS × 63.6 TB)"
db_nodes: 2
model: "Elastic per-server (X9M+: 2-32 DB servers, 3-64 storage servers). 'Quarter/half/full rack' terminology is legacy; X9M and X11M are billed per-server."
notes: |
Three different storage figures appear in Oracle datasheets — they measure different things:
• Total Usable Disk Capacity = ASM-allocatable space, post HIGH redundancy + drive-failure reserve, before compression
• Max DB Size No Local Backup = practical limit for DB allocation after RECO/SPARSE/DBFS reserves (this is what OCI Cost Estimator caps at)
• Max DB Size With Local Backup = ~half of above (FRA on disk for backups)
Always quote both Usable Disk and Max DB Size to avoid customer confusion.
OCI scales storage by adding individual storage servers, not in racks.
base_configuration:
description: "Minimum elastic configuration — what you get on day-1 before scaling."
db_servers: 2
storage_servers: 3
half_rack:
ocpus: "504 (X9M: 4 × 126/server)"
memory: "5.43 TB"
storage: "Up to 383 TB usable"
db_nodes: 4
storage_servers: 6
full_rack:
ocpus: "1,008 (X9M: 8 × 126/server)"
memory: "10.86 TB"
storage: "Up to 766 TB usable"
db_nodes: 8
storage_servers: 12
x11m_elastic_base:
description: "Datasheet 'Elastic Configuration Example 1' (2 DB + 3 SS, all-elastic SKUs)."
source: "ADB-D X11M Datasheet v1.0, Jan 2025, Table 1"
db_servers: 2
storage_servers: 3
total_ecpus: 1520 # 2 × 760
total_memory_gb: 2780 # 2 × 1,390
total_xrmem_tb: 3.75 # 3 × 1.25 (XRMEM lives on storage servers)
total_flash_tb: 81.6 # 3 × 27.2
hc_variant:
total_usable_disk_tb: 240 # Datasheet authoritative
max_db_size_no_local_backup_tb: 192 # OCI Cost Estimator caps allocation here
max_db_size_with_local_backup_tb: 96
max_sql_flash_bandwidth_gbps: 300
max_sql_xrmem_bandwidth_gbps: 1500
max_sql_disk_bandwidth_gbps: 5.4
max_sql_read_iops: 5_600_000
max_sql_write_iops: 3_000_000
max_sql_disk_iops: 7_800
max_data_load_rate_tb_per_hr: 7.5
ef_variant:
usable_tb: 157.5 # 3 × 52.5
sample_elastic_growth_examples:
compute_heavy: { db_servers: 8, storage_servers: 8, total_ecpus: 6080, max_db_size_tb: 512 }
storage_heavy: { db_servers: 2, storage_servers: 14, total_ecpus: 1520, max_db_size_tb: 896 }
x11m_base_system:
description: "Hardware-generation-agnostic 'Base System' SKU — fixed-spec, NOT expandable, single VM Cluster only. Different product from elastic; entry-price option for small footprints."
source: "ADB-D X11M Datasheet v1.0, Jan 2025, Table 1, footnote 1"
db_servers: 2
storage_servers: 3
total_ecpus: 192
total_memory_gb: 720
total_flash_tb: 38.4
total_usable_disk_tb: 73
max_db_size_no_local_backup_tb: 58
max_db_size_with_local_backup_tb: 29
network: "10 GbE (vs 50 GbE on elastic)"
notes: "Base System is NOT the same as 'Elastic Example 1 with 2 DB + 3 SS'. The elastic config has ~8× more ECPUs, ~4× more memory, ~3× more storage at same physical-server count, and 50 GbE networking. Base System is for cost-optimised entry; if customer expects to scale, go elastic from day 1."
x9m_base:
cpu_ocpus: 252 # 2 × 126
memory_tb: 2.71 # 2 × 1,390 GB
pmem_tb: 3.0 # 2 × 1.5 (persistent memory tier)
flash_cache_tb_db: 51.2 # 2 × 25.6
hc_variant:
usable_tb_per_ss: 63.6
base_usable_tb_high_redundancy: 190.8 # 3 × 63.6
notes: "X9M HC drives are 14 TB (vs 22 TB on X11M)."
legacy_x8_fixed_shapes:
quarter_rack:
storage_tb: 149
@@ -74,8 +139,9 @@ sizing:
full_rack:
storage_tb: 598
notes: "X8 shapes are fixed-size (not elastic). Storage values are usable disk, not flash cache."
ocpu_scaling: "OCPUs can be scaled online in increments without downtime. You pay for enabled OCPUs, not the total available in the rack."
sizing_guidance: "Start with a quarter rack for most production workloads. Scale OCPUs on demand. Choose half/full rack only when storage capacity or aggregate memory requirements exceed quarter rack limits."
ocpu_scaling: "OCPUs/ECPUs can be scaled online in increments without downtime. You pay for enabled cores, not the total available in the rack."
sizing_guidance: "Start with the elastic base (2 DB + 3 SS) for most production workloads. Add storage servers when capacity or IOPS exceed base. Add DB servers when CPU saturates. Choose HC over EF unless workload is latency-sensitive on cache-miss reads."
gotchas:
- id: exacs_minimum_cost
@@ -102,6 +168,10 @@ gotchas:
severity: LOW
description: "ExaCS requires placement in a private subnet with sufficient IP addresses. Each DB node and each virtual IP consumes an IP; plan your subnet CIDR accordingly."
- id: exacs_x11m_hc_vs_ef
severity: MEDIUM
description: "X11M ships in two storage variants — HC (default, 264 TB raw / SS) and EF (all-flash, 52.5 TB usable / SS). Most customers should choose HC: same hot-data latency (served from 27.2 TB flash cache per SS) at far better $/TB. Only pick EF when the working set genuinely exceeds flash cache and you need sub-ms consistent latency on cache-miss reads. Many quotes default to EF without justification — confirm with the customer."
implied_dependencies:
- service: vcn
reason: "ExaCS infrastructure must be deployed in a VCN"
@@ -123,3 +193,9 @@ changelog:
- date: "2026-03-14"
contributor: { name: "Diego Cabrera", team: "Field Architecture" }
change: "Initial creation with shapes, sizing, gotchas, dependencies"
- date: "2026-04-23"
contributor: { name: "Diego Velásquez", team: "Field Architecture" }
change: "Added X11M HC variant (default) alongside existing EF; replaced misleading rack_configurations table that mixed X9M data with elastic per-server model; added X11M and X9M base configs; added pricing verified against OCI Cost Estimator (DB+SS @ $2.9032/hr); added gotcha on HC vs EF default choice."
- date: "2026-04-23"
contributor: { name: "Diego Velásquez", team: "Field Architecture" }
change: "Replaced earlier estimator-derived storage figures (63 TB/SS, 190 TB at base) with authoritative numbers from ADB-D X11M Datasheet v1.0 Jan 2025 Table 1. X11M HC SS = 80 TB Total Usable Disk (HIGH redundancy + drive-failure reserve). 3-SS Elastic base = 240 TB Total Usable Disk OR 192 TB Max DB Size (no local backup, what the Estimator caps at) OR 96 TB Max DB Size (with local backup). Also added separate Base System SKU (fixed, non-expandable, 73 TB usable disk) — distinct product from elastic 2 DB + 3 SS. Added performance metrics (IOPS, GB/s) for elastic base."

View File

@@ -48,3 +48,4 @@ bom:
notes:
- "Pricing based on Ashburn region (OC1)"
- "Discounts subject to Oracle approval"
- "Hours convention — 730 vs 744: this BOM uses 730 hrs/month (= 8,760 annual hrs / 12), which equals the actual yearly OCI billing for hourly SKUs. OCI Cost Estimator defaults to 744 hrs/month (worst-case 31-day month); extrapolated to 12 months, 744 overstates the real annual cost by 1.92%. For 12-month TCO/proposals, 730 is the accurate figure; 744 is only appropriate when quoting a single peak month as an upper bound."

View File

@@ -328,6 +328,7 @@ class OCIBomGenerator:
data_start_row = row
cat_subtotal_rows = []
data_item_rows = [] # line-item rows (excludes category headers/subtotals/totals)
# Ensure categories present in items but missing from catalog order
# (e.g., "other" from unknown SKUs) still render at the end.
@@ -418,6 +419,7 @@ class OCIBomGenerator:
ws.cell(row=row, column=COL_CONV_WO).number_format = '#,##0.00'
ws.cell(row=row, column=COL_CONV_W).number_format = '#,##0.00'
data_item_rows.append(row)
row += 1
# Category subtotal row
@@ -492,10 +494,8 @@ class OCIBomGenerator:
# ── Cost proportion formulas (back-fill) ─────────────────
# Cost % = line monthly w/ discount / total monthly w/ discount
for item_row in range(data_start_row, total_row):
cell_val = ws.cell(row=item_row, column=COL_M_W).value
if cell_val and str(cell_val).startswith("=") and "SUM" not in str(cell_val):
mw_l = get_column_letter(COL_M_W)
for item_row in data_item_rows:
ws.cell(
row=item_row, column=COL_PCT,
value=f"=IF({mw_l}{total_row}=0,0,{mw_l}{item_row}/{mw_l}{total_row})",

View File

@@ -220,6 +220,13 @@ def refresh_catalog(verbose=False):
print(" Not in API (possibly retired): {}".format(not_found))
print(" Catalog saved: {}".format(CATALOG_PATH))
# Reverse-direction check: SKUs in API we haven't catalogued yet.
catalog_skus_after = {str(e["sku"]) for cat in raw.get("categories", {}).values()
for e in cat.get("skus", [])}
stub_map = {s: {} for s in catalog_skus_after}
new_skus = discover_missing_skus(api_map, stub_map, verbose=verbose)
print_missing_skus(new_skus, limit=None if verbose else 20)
def validate_catalog(verbose=False):
"""Compare current catalog prices against API and report differences."""
@@ -282,6 +289,106 @@ def validate_catalog(verbose=False):
if len(missing) > 10:
print(" ... and {} more".format(len(missing) - 10))
# Reverse-direction check: SKUs in API we haven't catalogued yet.
new_skus = discover_missing_skus(api_map, sku_map, verbose=verbose)
print_missing_skus(new_skus, limit=None if verbose else 20)
def discover_missing_skus(api_map, catalog_sku_map, verbose=False):
"""Find SKUs present in the API but missing from the catalog.
The filter is auto-derived: we collect every `serviceCategory` value for
SKUs already in the catalog and only report new SKUs whose serviceCategory
falls within that curated set. This keeps the report relevant — as new
categories are added to the catalog, the discovery scope expands.
"""
curated_categories = set()
for sku in catalog_sku_map:
if sku in api_map:
sc = api_map[sku].get("serviceCategory", "") or ""
if sc:
curated_categories.add(sc)
if verbose:
print(" Curated serviceCategory values ({}): {}".format(
len(curated_categories), ", ".join(sorted(curated_categories))
))
missing = []
for sku, product in api_map.items():
if sku in catalog_sku_map:
continue
sc = product.get("serviceCategory", "") or ""
if sc not in curated_categories:
continue
missing.append({
"sku": sku,
"product": product.get("displayName", ""),
"service_category": sc,
"metric": product.get("metricName", ""),
"price": extract_payg_price(product),
})
missing.sort(key=lambda x: (x["service_category"], x["sku"]))
return missing
def print_missing_skus(missing, limit=None):
"""Print a readable report of missing SKUs, grouped by serviceCategory."""
if not missing:
print("\nNo new SKUs discovered in curated categories. Catalog covers the API well.")
return
print("\n{} SKUs present in API but missing from catalog".format(len(missing)))
print("(filtered to serviceCategory values already represented in our catalog)\n")
by_cat = {}
for m in missing:
by_cat.setdefault(m["service_category"], []).append(m)
shown = 0
truncated = False
for cat, items in sorted(by_cat.items()):
print(" [{}] ({} new)".format(cat, len(items)))
for m in items:
if limit is not None and shown >= limit:
truncated = True
break
price_str = "${:.4f}".format(m["price"]) if m["price"] else " -"
print(" {:<10} {:>10} {}".format(
m["sku"], price_str, m["product"][:70]
))
shown += 1
if truncated:
break
if truncated:
print("\n ... and {} more (re-run with -v to list all)".format(len(missing) - shown))
print("\nNext step: review and add relevant entries to kb/pricing/oci-sku-catalog.yaml")
print(" under the appropriate category block.")
def discover_catalog(verbose=False):
"""Stand-alone --discover entry point: report new API SKUs not yet in catalog."""
print("Fetching current products from Oracle API...")
products = fetch_all_products(currency=DEFAULT_CURRENCY, verbose=verbose)
api_map = {}
for p in products:
pn = p.get("partNumber", "")
if pn:
api_map[pn] = p
raw, sku_map = load_current_catalog()
if not raw:
print("Error: Could not load catalog at {}".format(CATALOG_PATH))
return 1
missing = discover_missing_skus(api_map, sku_map, verbose=verbose)
print_missing_skus(missing, limit=None if verbose else 40)
# Machine-readable summary for CI/automation — always last line on stdout.
print("\nDISCOVER_MISSING_COUNT={}".format(len(missing)))
return 0
def inspect_sku(part_number):
"""Fetch and display a single SKU from the API."""
@@ -571,6 +678,9 @@ def main():
"(currently: compute)")
parser.add_argument("--validate", action="store_true",
help="Validate catalog prices against API")
parser.add_argument("--discover", action="store_true",
help="Report SKUs present in API but missing from catalog "
"(filtered to serviceCategory values already curated)")
parser.add_argument("--sku", type=str,
help="Inspect a single SKU from the API")
parser.add_argument("--dump", type=str, metavar="FILE",
@@ -589,6 +699,8 @@ def main():
refresh_catalog(verbose=args.verbose or args.diff)
elif args.validate:
validate_catalog(verbose=args.verbose)
elif args.discover:
return discover_catalog(verbose=args.verbose)
elif args.dump:
dump_all(args.dump, verbose=args.verbose)
else: