Compare commits
8 Commits
9d5cda8986
...
automation
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32319b67d7 | ||
|
|
9fbe05a6a5 | ||
|
|
9b4d04cf34 | ||
|
|
30394ece48 | ||
|
|
480c4bca52 | ||
|
|
433b08071b | ||
|
|
874d40d38a | ||
|
|
a22711c065 |
@@ -19,13 +19,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update -qq
|
apt-get update -qq
|
||||||
sudo apt-get install -y python3 python3-pip
|
apt-get install -y --no-install-recommends python3 python3-requests python3-bs4 python3-yaml
|
||||||
python3 --version
|
python3 --version
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: python3 -m pip install --user requests beautifulsoup4 pyyaml
|
|
||||||
|
|
||||||
- name: Check Architecture Center links
|
- name: Check Architecture Center links
|
||||||
id: check
|
id: check
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
@@ -71,7 +68,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload report
|
- name: Upload report
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: kb-health-report
|
name: kb-health-report
|
||||||
path: /tmp/link-check.txt
|
path: /tmp/link-check.txt
|
||||||
|
|||||||
147
.gitea/workflows/sku-catalog-refresh.yaml
Normal file
147
.gitea/workflows/sku-catalog-refresh.yaml
Normal 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
|
||||||
5
Makefile
5
Makefile
@@ -1,6 +1,6 @@
|
|||||||
# OCI Deal Accelerator — Build Automation
|
# 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
|
# Use venv if present, otherwise find best available python3
|
||||||
ifneq (,$(wildcard .venv/bin/python))
|
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
|
freshness-refresh: ## Run refresh tools for stale KB files that support automation
|
||||||
@$(PYTHON) tools/kb_freshness.py --auto-refresh
|
@$(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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
last_verified: '2026-03-27'
|
last_verified: '2026-04-24'
|
||||||
source: Oracle OCI Price List + internal BOM reference v1.73
|
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
|
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/
|
only. Always verify against https://www.oracle.com/cloud/pricing/
|
||||||
@@ -560,6 +560,18 @@ categories:
|
|||||||
list_price_usd: 0.0807
|
list_price_usd: 0.0807
|
||||||
default_hours_units: 730
|
default_hours_units: 730
|
||||||
billing_type: hourly
|
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:
|
exascale:
|
||||||
display_name: Oracle Cloud Infrastructure - Database Exadata Exascale Infrastructure
|
display_name: Oracle Cloud Infrastructure - Database Exadata Exascale Infrastructure
|
||||||
skus:
|
skus:
|
||||||
@@ -1289,12 +1301,6 @@ categories:
|
|||||||
list_price_usd: 0.015
|
list_price_usd: 0.015
|
||||||
default_hours_units: 730
|
default_hours_units: 730
|
||||||
billing_type: hourly
|
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
|
- sku: EST-DS-NOTEBOOK
|
||||||
product: '[ESTIMATE] OCI Data Science - Notebook Session (billed via underlying VM.Standard compute OCPU)'
|
product: '[ESTIMATE] OCI Data Science - Notebook Session (billed via underlying VM.Standard compute OCPU)'
|
||||||
metric: OCPU Per Hour
|
metric: OCPU Per Hour
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
---
|
---
|
||||||
last_verified: 2026-03-14
|
last_verified: 2026-04-24
|
||||||
service: OCI Compute
|
service: OCI Compute
|
||||||
category: compute
|
category: compute
|
||||||
oci_color: "#4F7B6E"
|
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
|
what: Virtual machines and bare metal instances. Flexible shapes allow custom
|
||||||
@@ -42,6 +45,24 @@ shape_families:
|
|||||||
max_ocpu: 18
|
max_ocpu: 18
|
||||||
memory_per_ocpu_gb: "1-64 GB per OCPU"
|
memory_per_ocpu_gb: "1-64 GB per OCPU"
|
||||||
use_for: "High-frequency trading, HPC"
|
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
|
- name: BM.Standard.E5
|
||||||
processor: "AMD EPYC 9J14 (Genoa)"
|
processor: "AMD EPYC 9J14 (Genoa)"
|
||||||
ocpus: 192
|
ocpus: 192
|
||||||
@@ -50,6 +71,30 @@ shape_families:
|
|||||||
processor: "AMD EPYC 9J45 (Turin)"
|
processor: "AMD EPYC 9J45 (Turin)"
|
||||||
ocpus: 256
|
ocpus: 256
|
||||||
use_for: "Bare metal latest generation"
|
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
|
- name: VM.GPU.A10.1
|
||||||
processor: "NVIDIA A10"
|
processor: "NVIDIA A10"
|
||||||
gpu_count: 1
|
gpu_count: 1
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# OCI Service: Exadata Cloud Service (ExaCS)
|
# OCI Service: Exadata Cloud Service (ExaCS)
|
||||||
# Last verified: 2026-03
|
# Last verified: 2026-04
|
||||||
|
|
||||||
service:
|
service:
|
||||||
name: "Exadata Cloud Service (ExaCS)"
|
name: "Exadata Cloud Service (ExaCS)"
|
||||||
@@ -35,37 +35,102 @@ variants:
|
|||||||
usable_tb: 63.6
|
usable_tb: 63.6
|
||||||
- id: exacs_x11m
|
- id: exacs_x11m
|
||||||
name: "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:
|
per_db_server:
|
||||||
ecpus: 760
|
ecpus: 760
|
||||||
memory_gb: 1390
|
memory_gb: 1390
|
||||||
xrmem_gb: 1250
|
xrmem_gb: 1250
|
||||||
flash_cache_tb: 27.2
|
flash_cache_tb: 27.2
|
||||||
per_storage_server:
|
cpu: "2 × AMD EPYC 9J25 (96 cores each, 192 cores per server)"
|
||||||
usable_tb: 52.5
|
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)"
|
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:
|
sizing:
|
||||||
model: "Elastic per-server (2-32 DB servers, 3-64 storage servers)"
|
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."
|
||||||
rack_configurations:
|
notes: |
|
||||||
quarter_rack:
|
Three different storage figures appear in Oracle datasheets — they measure different things:
|
||||||
ocpus: "252 (X9M: 2 × 126/server)"
|
• Total Usable Disk Capacity = ASM-allocatable space, post HIGH redundancy + drive-failure reserve, before compression
|
||||||
memory: "2.71 TB (X9M: 2 × 1,390 GB)"
|
• Max DB Size – No Local Backup = practical limit for DB allocation after RECO/SPARSE/DBFS reserves (this is what OCI Cost Estimator caps at)
|
||||||
storage: "Up to 190.8 TB usable (X9M: 3 SS × 63.6 TB)"
|
• Max DB Size – With Local Backup = ~half of above (FRA on disk for backups)
|
||||||
db_nodes: 2
|
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
|
storage_servers: 3
|
||||||
half_rack:
|
|
||||||
ocpus: "504 (X9M: 4 × 126/server)"
|
x11m_elastic_base:
|
||||||
memory: "5.43 TB"
|
description: "Datasheet 'Elastic Configuration Example 1' (2 DB + 3 SS, all-elastic SKUs)."
|
||||||
storage: "Up to 383 TB usable"
|
source: "ADB-D X11M Datasheet v1.0, Jan 2025, Table 1"
|
||||||
db_nodes: 4
|
db_servers: 2
|
||||||
storage_servers: 6
|
storage_servers: 3
|
||||||
full_rack:
|
total_ecpus: 1520 # 2 × 760
|
||||||
ocpus: "1,008 (X9M: 8 × 126/server)"
|
total_memory_gb: 2780 # 2 × 1,390
|
||||||
memory: "10.86 TB"
|
total_xrmem_tb: 3.75 # 3 × 1.25 (XRMEM lives on storage servers)
|
||||||
storage: "Up to 766 TB usable"
|
total_flash_tb: 81.6 # 3 × 27.2
|
||||||
db_nodes: 8
|
hc_variant:
|
||||||
storage_servers: 12
|
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:
|
legacy_x8_fixed_shapes:
|
||||||
quarter_rack:
|
quarter_rack:
|
||||||
storage_tb: 149
|
storage_tb: 149
|
||||||
@@ -74,8 +139,9 @@ sizing:
|
|||||||
full_rack:
|
full_rack:
|
||||||
storage_tb: 598
|
storage_tb: 598
|
||||||
notes: "X8 shapes are fixed-size (not elastic). Storage values are usable disk, not flash cache."
|
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:
|
gotchas:
|
||||||
- id: exacs_minimum_cost
|
- id: exacs_minimum_cost
|
||||||
@@ -102,6 +168,10 @@ gotchas:
|
|||||||
severity: LOW
|
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."
|
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:
|
implied_dependencies:
|
||||||
- service: vcn
|
- service: vcn
|
||||||
reason: "ExaCS infrastructure must be deployed in a VCN"
|
reason: "ExaCS infrastructure must be deployed in a VCN"
|
||||||
@@ -123,3 +193,9 @@ changelog:
|
|||||||
- date: "2026-03-14"
|
- date: "2026-03-14"
|
||||||
contributor: { name: "Diego Cabrera", team: "Field Architecture" }
|
contributor: { name: "Diego Cabrera", team: "Field Architecture" }
|
||||||
change: "Initial creation with shapes, sizing, gotchas, dependencies"
|
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."
|
||||||
|
|||||||
@@ -48,3 +48,4 @@ bom:
|
|||||||
notes:
|
notes:
|
||||||
- "Pricing based on Ashburn region (OC1)"
|
- "Pricing based on Ashburn region (OC1)"
|
||||||
- "Discounts subject to Oracle approval"
|
- "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."
|
||||||
|
|||||||
@@ -328,6 +328,7 @@ class OCIBomGenerator:
|
|||||||
|
|
||||||
data_start_row = row
|
data_start_row = row
|
||||||
cat_subtotal_rows = []
|
cat_subtotal_rows = []
|
||||||
|
data_item_rows = [] # line-item rows (excludes category headers/subtotals/totals)
|
||||||
|
|
||||||
# Ensure categories present in items but missing from catalog order
|
# Ensure categories present in items but missing from catalog order
|
||||||
# (e.g., "other" from unknown SKUs) still render at the end.
|
# (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_WO).number_format = '#,##0.00'
|
||||||
ws.cell(row=row, column=COL_CONV_W).number_format = '#,##0.00'
|
ws.cell(row=row, column=COL_CONV_W).number_format = '#,##0.00'
|
||||||
|
|
||||||
|
data_item_rows.append(row)
|
||||||
row += 1
|
row += 1
|
||||||
|
|
||||||
# Category subtotal row
|
# Category subtotal row
|
||||||
@@ -492,10 +494,8 @@ class OCIBomGenerator:
|
|||||||
|
|
||||||
# ── Cost proportion formulas (back-fill) ─────────────────
|
# ── Cost proportion formulas (back-fill) ─────────────────
|
||||||
# Cost % = line monthly w/ discount / total monthly w/ discount
|
# 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)
|
mw_l = get_column_letter(COL_M_W)
|
||||||
|
for item_row in data_item_rows:
|
||||||
ws.cell(
|
ws.cell(
|
||||||
row=item_row, column=COL_PCT,
|
row=item_row, column=COL_PCT,
|
||||||
value=f"=IF({mw_l}{total_row}=0,0,{mw_l}{item_row}/{mw_l}{total_row})",
|
value=f"=IF({mw_l}{total_row}=0,0,{mw_l}{item_row}/{mw_l}{total_row})",
|
||||||
|
|||||||
@@ -220,6 +220,13 @@ def refresh_catalog(verbose=False):
|
|||||||
print(" Not in API (possibly retired): {}".format(not_found))
|
print(" Not in API (possibly retired): {}".format(not_found))
|
||||||
print(" Catalog saved: {}".format(CATALOG_PATH))
|
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):
|
def validate_catalog(verbose=False):
|
||||||
"""Compare current catalog prices against API and report differences."""
|
"""Compare current catalog prices against API and report differences."""
|
||||||
@@ -282,6 +289,106 @@ def validate_catalog(verbose=False):
|
|||||||
if len(missing) > 10:
|
if len(missing) > 10:
|
||||||
print(" ... and {} more".format(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):
|
def inspect_sku(part_number):
|
||||||
"""Fetch and display a single SKU from the API."""
|
"""Fetch and display a single SKU from the API."""
|
||||||
@@ -571,6 +678,9 @@ def main():
|
|||||||
"(currently: compute)")
|
"(currently: compute)")
|
||||||
parser.add_argument("--validate", action="store_true",
|
parser.add_argument("--validate", action="store_true",
|
||||||
help="Validate catalog prices against API")
|
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,
|
parser.add_argument("--sku", type=str,
|
||||||
help="Inspect a single SKU from the API")
|
help="Inspect a single SKU from the API")
|
||||||
parser.add_argument("--dump", type=str, metavar="FILE",
|
parser.add_argument("--dump", type=str, metavar="FILE",
|
||||||
@@ -589,6 +699,8 @@ def main():
|
|||||||
refresh_catalog(verbose=args.verbose or args.diff)
|
refresh_catalog(verbose=args.verbose or args.diff)
|
||||||
elif args.validate:
|
elif args.validate:
|
||||||
validate_catalog(verbose=args.verbose)
|
validate_catalog(verbose=args.verbose)
|
||||||
|
elif args.discover:
|
||||||
|
return discover_catalog(verbose=args.verbose)
|
||||||
elif args.dump:
|
elif args.dump:
|
||||||
dump_all(args.dump, verbose=args.verbose)
|
dump_all(args.dump, verbose=args.verbose)
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user