14 Commits

Author SHA1 Message Date
sku-refresh-bot
f95c4c3dc8 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:33:17 +00: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
root
9d5cda8986 Fix BOM workbook monthly totals
All checks were successful
Deploy Skill to OCI / deploy (push) Successful in 21s
2026-04-23 10:54:07 -03:00
root
6de2251e00 Fix slide and diagram bugs in deck/diagram generators
Service Tiering slide no longer falls back to the workload `name`
field for the tier label, which previously rendered "Bronze Bronze"
when the spec only carried tier names. Uptime/RTO/RPO now fill from
tier defaults when the spec omits them.

Architecture Principles slide enriches caller-supplied items with
name + summary from the KB when only the principle id is given,
instead of rendering bare placeholders like "P-01 P-02".

Architecture Overview slide auto-builds a two-region visual when the
proposal spec names a `dr_region` but doesn't pre-render `architecture.visual`,
so DR is no longer dropped from the deck.

Diagram generator gained a region-level `local_dr` flag that adds a
"Local DR Standby" dormant node inside the region, and now auto-wires
an "RPC (Remote Peering)" edge between DRGs when the spec defines
two or more regions with DRGs but no manual peering connection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 00:35:10 -03:00
root
77298a471c Read DR region from spec + make operational_raci handler tolerant
All checks were successful
Deploy Skill to OCI / deploy (push) Successful in 19s
Architecture Overview slide was rendering the DR region with a fallback
label (e.g. "DR STANDBY" with no region name) because _adapt_flat_spec()
only consulted disaster_recovery / architecture.dr — it never looked at
the dr_region / tenancy.regions fields the MCP actually emits. The
adapter now discovers the primary and DR regions from:

- spec.primary_region / spec.region / architecture.primary_region
- spec.dr_region / spec.secondary_region / architecture.dr_region
- tenancy.regions[] entries (strings or {region, role} dicts, with role
  in {primary, hub, prod, production} vs {dr, standby, secondary,
  failover})

When both regions are known, the adapter emits an architecture.visual
with two labeled region blocks so the DR region renders with its real
name (e.g. "mx-monterrey-1 [DR STANDBY]") instead of the placeholder.
If the spec already supplies architecture.visual or diagram_path, those
are preserved instead of overwritten.

Operational RACI slide kept being skipped for payloads that passed
operational_raci in a non-canonical shape. The from_spec handler now
accepts:

- Top-level key aliases: operational_raci, raci, operations_raci
- Bare string ("fully_managed") → loads KB defaults for that model
- List of dicts → used directly as raci_items
- Dict with raci_items / items / activities and model / engagement_model
- Empty raci_items → falls back to KB defaults by model

The flat adapter also now respects a user-supplied operational_raci
instead of overwriting it with the co_managed default. Verified:
operational_raci: "fully_managed" renders the Fully-Managed RACI table
with 18 activities; bare flat MCP payload still gets the 13-activity
co_managed default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 14:32:25 -03:00
root
6209907a63 Enrich MCP flat-spec adapter to emit full ECAL deck content
All checks were successful
Deploy Skill to OCI / deploy (push) Successful in 17s
Regression after the closing-slide removal surfaced a separate bug:
_adapt_flat_spec() only emitted metadata + summary + cost, so decks
generated from the MCP flat payload collapsed to 3-5 slides of title +
summary. The deck generator's ECAL sections (service_tiering,
architecture_principles, environment_catalogue, operational_raci, etc.)
all check top-level spec keys the adapter never produced.

Adapter now also emits, from the flat payload + kb/patterns defaults:
- service_tiering from services[] (preserves any tier/uptime/rto/rpo)
- architecture / architecture_principles (ECAL "always" picks)
- ha_dr tiers derived from the distinct service tiers present
- security baseline (IAM/network/database/monitoring controls)
- environment_catalogue (Prod/Pre-Prod/Dev-Test, +DR if enabled)
- operational_raci co_managed default from the KB
- next_steps skeleton

Flat MCP payload now renders 11 content slides instead of 3-5, and the
proposal-spec.yaml path (non-flat) is unchanged — still 16 slides for
examples/proposal-spec.yaml.

Also documents the new data_services SKUs in docs/bom-cookbook.md as
Recipe 4 (BDS/DS/DF) so MCP payloads can use the catalog codes directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 13:13:42 -03:00
root
fc550c14bb Drop closing blanks, honor primary_driver, catalog data-services SKUs
All checks were successful
Deploy Skill to OCI / deploy (push) Successful in 23s
- oci_deck_gen / oci_bizcase_gen: stop appending the Thank You + Oracle
  logo-only closing slides at the end of every generated deck.
- oci_bizcase_gen: Business Drivers now reads primary_driver at the
  drivers-level and at the top level, and renders natural-language values
  verbatim (only snake_case enum tokens are Title-cased). A spec-provided
  drivers.items / additional / secondary list replaces the hardcoded
  "Financial Impact of Inaction" + "Operational Impact" fallback cards.
- oci-sku-catalog.yaml: add data_services category with the four real
  Big Data Service SKUs (B91128/B91129/B91130/B93555) plus EST-DS-NOTEBOOK,
  EST-DS-MODEL, EST-DF-SPARK as estimate placeholders (OCI Data Science /
  Data Flow have no dedicated SKUs in the public pricing API — they are
  billed via the underlying compute shape).
- oci_bom_gen: resolver now prints a stderr warning and tags estimate/
  unknown line items with an explicit note; the xlsx writer renders
  "other" category items under an "Uncategorized — confirm SKUs" section
  at the end instead of silently dropping them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 12:34:37 -03:00
root
490072a4b7 Accept flat-spec input aliases across generators
All checks were successful
Deploy Skill to OCI / deploy (push) Successful in 20s
Harden bizcase, bom, deck, and diagram generators to tolerate payload
shape variants (metadata/cover/summary/line_item aliases, current_state
as string, alternate pillar keys) so MCP and CLI flat specs render
consistently. Add input-alias tests per generator.

Also loosen KB governance tests to handle multi-document service YAMLs
with optional changelogs, untrack the customer demo output under
examples/output-demo-pharma-mx/ (matches .gitignore), and ship an
ADB-S vs Aurora 500GB sample deck.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 10:22:14 -03:00
root
bcb16795d1 Add BOM cookbook to shortcut SKU mapping for common patterns
All checks were successful
Deploy Skill to OCI / deploy (push) Successful in 24s
Codex sessions burn 10+ tool calls grepping kb/field-knowledge and the SKU
catalog to map standard requirements (ExaCS X11M BYOL, ADB-D, ADB-S + block
+ FastConnect) to the 4-5 SKUs the MCP BOM tools need. Bake the recipes
into docs/bom-cookbook.md so the LLM can skip the exploration loop for
well-known patterns, and link it from SKILL.md between Output Generation
and Knowledge Base (same progressive-disclosure pattern the other docs use).

Recipe 2 (ADB-Dedicated) explicitly calls out the infra-SKU overlap with
ExaCS — one of the specific traps the exploration loop keeps hitting.
Catalog-fallback note keeps the cookbook honest when a requirement does
not match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:29:20 -03:00
root
e57ab57b48 Accept MCP flat payload in deck generator to stop silent-blank decks
generate_deck on the MCP server passes a flat spec (customer_name, title,
workload_type, services, cost_summary, ...) — but from_spec only understood
the proposal-spec YAML shape (metadata/summary/architecture/...). With no
matching keys, only the title and closing layouts rendered, both populated
with empty strings, producing a deck that looked blank.

Detect the flat shape in from_spec and adapt it into the proposal-spec
fields that the existing slide builders already consume. Title, summary
(target state + current-state bullets) and cost slides now render from
whatever the MCP tool sends; the richer proposal-spec path is untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:27:44 -03:00
33 changed files with 2270 additions and 2263 deletions

View File

@@ -74,7 +74,40 @@
"WebFetch(domain:github.com)",
"Bash(curl -s --max-time 15 -H \"X-Oracle-Accept-CurrencyCode: USD\" \"https://itra.oraclecloud.com/itas/.anon/myservices/api/v1/products?limit=2&partNumber=B110627\")",
"Bash(curl -v --max-time 10 -H \"X-Oracle-Accept-CurrencyCode: USD\" \"https://itra.oraclecloud.com/itas/.anon/myservices/api/v1/products?limit=1\")",
"Bash(curl -s --max-time 15 \"https://apexapps.oracle.com/pls/apex/cetools/api/v1/products/?partNumber=B110627&currencyCode=USD\")"
"Bash(curl -s --max-time 15 \"https://apexapps.oracle.com/pls/apex/cetools/api/v1/products/?partNumber=B110627&currencyCode=USD\")",
"Bash(find:*)",
"Bash(echo \"exit=$?\")",
"Bash(echo \"quiet exit=$?\")",
"Bash(make freshness:*)",
"Bash(echo \"freshness exit=$?\")",
"Bash(make help:*)",
"Bash(make lint:*)",
"Bash(curl -sS \"https://apexapps.oracle.com/pls/apex/cetools/api/v1/products/?currencyCode=USD\")",
"Bash(/bin/rm kb/pricing/ai-ml.yaml kb/pricing/analytics.yaml kb/pricing/containers-serverless.yaml kb/pricing/database.yaml kb/pricing/developer.yaml kb/pricing/hybrid-vmware.yaml kb/pricing/integration.yaml kb/pricing/networking.yaml kb/pricing/observability.yaml kb/pricing/pricing-models.yaml kb/pricing/security.yaml kb/pricing/storage.yaml)",
"Bash(python3.12 tools/oci_deck_gen.py --spec examples/proposal-spec.yaml --output examples/output-cli-improvements-deliverables/architecture-proposal.pptx)",
"Bash(python3.12 tools/oci_diagram_gen.py --spec examples/diagram-spec.yaml --output examples/output-cli-improvements-deliverables/architecture.drawio)",
"Bash(python3.12 tools/oci_pdf_gen.py --spec examples/proposal-spec.yaml --output examples/output-cli-improvements-deliverables/customer-proposal.pdf)",
"Bash(python3.12 tools/oci_bom_gen.py --spec examples/exacs-bom-spec.yaml --output examples/output-cli-improvements-deliverables/exacs-bom.xlsx)",
"Bash(python3.12 tools/oci_bom_gen.py --spec examples/exacs-x11m-bom-spec.yaml --output examples/output-cli-improvements-deliverables/exacs-x11m-bom.xlsx)",
"Bash(python3.12 tools/kb_cli.py search \"Autonomous Database\" --json)",
"Bash(python3.12 tools/findings_cli.py search \"adb\" --product ADB-S --json)",
"Bash(python3.12 tools/findings_cli.py stats --json)",
"Bash(python3.12 tools/kb_cli.py arch-search --services adb-s exacs --tags ha-dr --json)",
"Bash(python3.12 tools/kb_cli.py ecal-score --summary \"ADB-S migration with Data Guard business case architecture review customer profile strategy map handover go-live\" --json)",
"Bash(python3.12 tools/oci_bom_gen.py --spec examples/exacs-bom-spec.yaml --output examples/output-cli-improvements-deliverables/exacs-bom-appca.xlsx --appca)",
"Bash(python3.12 tools/oci_bom_gen.py --spec examples/exacs-x11m-bom-spec.yaml --output examples/output-cli-improvements-deliverables/exacs-x11m-bom-appca.xlsx --appca)",
"Bash(python3.12:*)",
"Bash(pip3.12 list:*)",
"Bash(pip3.12 install:*)",
"Bash(xargs ls:*)",
"WebFetch(domain:www.drawio.com)",
"WebFetch(domain:jgraph.github.io)",
"WebFetch(domain:raw.githubusercontent.com)",
"Bash(ls /root/.claude/projects/-mnt-c-Users-Diego-Documents-oci-deal-accelerator-oracle-oci-deal-accelerator/a0d00b4c-8d61-421c-8ba9-19bb85f15fdb/tool-results/*.zip)",
"WebFetch(domain:developers.openai.com)",
"Bash(echo \"---exit:$?---\")",
"Bash(date +%s)",
"Read(//tmp/**)"
]
}
}

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,146 @@
# 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=$(grep -oE '^[0-9]+ SKUs present in API' /tmp/discover.log \
| head -1 | awk '{print $1}' || echo "0")
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}"
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

@@ -432,6 +432,14 @@ See [docs/skill/output-formats.md](docs/skill/output-formats.md) for the per-cus
---
## Cookbook: Building Tool Payloads
Before mapping customer requirements to SKUs for `generate_bom`, `generate_bom_appca`, or `generate_cost_estimate`, check [docs/bom-cookbook.md](docs/bom-cookbook.md) — it has copy-paste recipes for common patterns (ExaCS X11M BYOL, ADB-Dedicated, ADB-S + Block + FastConnect) and explicitly names the gotchas that otherwise burn exploration turns (e.g. ADB-Dedicated shares infrastructure SKUs with ExaCS — there are no separate `adb_dedicated` SKUs in the catalog).
If a requirement does not match a recipe, `grep` [`kb/pricing/oci-sku-catalog.yaml`](kb/pricing/oci-sku-catalog.yaml) and consult `kb/services/` before inventing SKUs.
---
## Knowledge Base
KB lives under `kb/`. See [kb/README.md](kb/README.md) for the directory map, frontmatter requirements, refresh tooling, and contributor guide.

190
docs/bom-cookbook.md Normal file
View File

@@ -0,0 +1,190 @@
# BOM Cookbook — Building `generate_bom` Payloads
Copy-paste recipes that map common customer requirements to the exact OCI SKUs the
`generate_bom`, `generate_bom_appca`, and `generate_cost_estimate` MCP tools expect.
The goal is to skip the "grep the catalog and guess SKUs" exploration loop that
burns tool calls on well-known patterns.
> **If the requirement does not match any recipe below, consult
> [`kb/services/`](../kb/services/) and
> [`kb/pricing/oci-sku-catalog.yaml`](../kb/pricing/oci-sku-catalog.yaml)
> before inventing SKUs.** All SKUs below are verified against the catalog as of
> 2026-04-20; re-`grep` the catalog if a recipe ever fails.
## Payload Shape
All three tools accept the same envelope:
```json
{
"customer_id": "acme-prod",
"discount_pct": 0.35,
"currency": "USD",
"services": [
{ "sku": "B90777", "quantity": 1 }
]
}
```
- `customer_id`: free-form identifier, used for output file naming.
- `discount_pct`: decimal (0.35 = 35%). Applied to every line unless overridden.
- `services[].quantity`: integer or float depending on the SKU metric (ECPUs, GB, hours).
---
## Recipe 1 — Exadata Cloud Service (ExaCS) X11M, BYOL
**When it matches:** "ExaCS X11M BYOL, N ECPUs, M TB storage, no DR" — customer-managed
Oracle Database on dedicated Exadata hardware, BYOL licensing.
**SKUs:**
| SKU | Component | Typical qty |
|-----|-----------|-------------|
| `B90777` | Exadata Infrastructure — Base System | `1` per rack |
| `B110627` | Exadata Database Server — X11M | `2` (Base/Quarter) — adjust per rack config |
| `B110629` | Exadata Storage Server — X11M | `3` (Base/Quarter) — adjust per rack config |
| `B110632` | Exadata Database ECPU — Dedicated Infra BYOL (X11M) | One per ECPU |
Swap `B110632``B110631` for PAYG (license-included) pricing.
**Payload (120 ECPUs, 120 TB, BYOL, standard Base System):**
```json
{
"customer_id": "acme-exacs",
"discount_pct": 0.35,
"currency": "USD",
"services": [
{ "sku": "B90777", "quantity": 1 },
{ "sku": "B110627", "quantity": 2 },
{ "sku": "B110629", "quantity": 3 },
{ "sku": "B110632", "quantity": 120 }
]
}
```
---
## Recipe 2 — Autonomous Database Dedicated (ADB-D), BYOL
**When it matches:** "ADB Dedicated, N ECPUs, M TB, BYOL" — Autonomous Database running
on dedicated Exadata infrastructure.
**⚠️ Gotcha that burns tool calls:** ADB-Dedicated has **no separate infrastructure
SKUs** in the catalog. It reuses the same Exadata dedicated infra SKUs as ExaCS
(`B90777`, `B110627`, `B110629`) plus the same Exadata ECPU SKU (`B110631`/`B110632`).
The Autonomous-vs-customer-managed distinction is a tenancy-level configuration,
not a billing line item. Do not search for `autonomous_dedicated` SKUs — they do
not exist.
**SKUs:** identical to Recipe 1.
**Payload (64 ECPUs, BYOL):**
```json
{
"customer_id": "acme-adbd",
"discount_pct": 0.35,
"currency": "USD",
"services": [
{ "sku": "B90777", "quantity": 1 },
{ "sku": "B110627", "quantity": 2 },
{ "sku": "B110629", "quantity": 3 },
{ "sku": "B110632", "quantity": 64 }
]
}
```
---
## Recipe 3 — Autonomous Database Serverless (ADB-S) + Block Volume + FastConnect
**When it matches:** "ADB-S ATP, N ECPUs, M TB data + block storage for app tier +
FastConnect to on-prem" — the typical "app on OCI Compute, DB on ADB-S" pattern.
**SKUs:**
| SKU | Component | Metric |
|-----|-----------|--------|
| `B95701` | ADB ECPU (ATP or ADW) — license-included | ECPU/hour |
| `B95703` | ADB ECPU BYOL — use instead of B95701 if BYOL | ECPU/hour |
| `B95706` | ADB ATP data storage | GB/month |
| `B95754` | ADB ADW data storage — use instead of B95706 for warehouse | GB/month |
| `B95754a` | ADB backup storage | GB/month |
| `B91961` | Block Volume storage (app tier) | GB/month |
| `B91962` | Block Volume performance (VPU) — optional, for higher IOPS tiers | VPU/GB/month |
| `B88326` | FastConnect 10 Gbps port | port-hour |
Swap `B88326``B88325` (1 Gbps) or `B93126` (100 Gbps) as needed.
**Payload (16 ECPUs ATP license-included, 2 TB data + 1 TB backup, 500 GB block, 10 G FastConnect):**
```json
{
"customer_id": "acme-adbs",
"discount_pct": 0.35,
"currency": "USD",
"services": [
{ "sku": "B95701", "quantity": 16 },
{ "sku": "B95706", "quantity": 2048 },
{ "sku": "B95754a", "quantity": 1024 },
{ "sku": "B91961", "quantity": 500 },
{ "sku": "B88326", "quantity": 1 }
]
}
```
---
## Recipe 4 — Big Data Service + Data Science + Data Flow (analytics stack)
**When it matches:** "HDFS-managed cluster for ETL", "notebooks + model deployment",
"managed Spark pipelines" — typical OCI analytics/AI workload mix.
**SKUs:**
| SKU | Component | Metric |
|-----|-----------|--------|
| `B91128` | Big Data Service — Compute Standard (real) | OCPU/hour |
| `B91129` | Big Data Service — Compute Dense I/O (real) | OCPU/hour |
| `B91130` | Big Data Service — Compute HPC (real) | OCPU/hour |
| `B93555` | Big Data Service — Core (real) | OCPU/hour |
| `EST-DS-NOTEBOOK` | **[Estimate]** Data Science notebook session — priced as underlying VM.Standard OCPU | OCPU/hour |
| `EST-DS-MODEL` | **[Estimate]** Data Science model-deployment endpoint — priced as underlying VM.Standard OCPU | OCPU/hour |
| `EST-DF-SPARK` | **[Estimate]** Data Flow managed Spark runtime — priced as underlying VM.Standard OCPU | OCPU/hour |
OCI Data Science and OCI Data Flow have no dedicated SKUs in the Oracle public
pricing API — both are billed via the underlying Compute shape + Block/Object
Storage consumed. The `EST-*` entries are placeholder estimates (VM.Standard3.Flex
OCPU rate) so BOMs can include them as line items with an explicit "estimate"
flag; confirm the actual shape and hours with the customer before quoting.
**Payload (16 BDS Standard + 8 BDS Dense I/O + 4 notebook + 2 model + 8 Spark):**
```json
{
"customer_id": "acme-analytics",
"discount_pct": 0.30,
"currency": "USD",
"services": [
{ "sku": "B91128", "quantity": 16 },
{ "sku": "B91129", "quantity": 8 },
{ "sku": "EST-DS-NOTEBOOK", "quantity": 4 },
{ "sku": "EST-DS-MODEL", "quantity": 2 },
{ "sku": "EST-DF-SPARK", "quantity": 8 }
]
}
```
Add `B91961` (Block Volume) and `B91628` (Object Storage Standard) when the
customer needs dedicated storage beyond the compute default.
---
## When a recipe doesn't fit
1. Grep the catalog: `grep -n "<product keyword>" kb/pricing/oci-sku-catalog.yaml`
2. Check service docs under `kb/services/` for deployment-specific guidance.
3. Check `kb/field-knowledge/` for known sizing/pricing gotchas.
4. If a SKU still cannot be located, surface the gap to the user rather than guessing — fabricated SKUs break the BOM silently.

View File

@@ -1,142 +0,0 @@
# OCI Deal Accelerator — BOM Spec
# Demo: PharmaCorp Mexico — ExaCS X11M + networking + security
# SKUs verified against kb/pricing/oci-sku-catalog.yaml
metadata:
customer: "PharmaCorp Mexico"
prepared_by: "Carlos Mendoza — Solutions Architect"
date: "2026-04-12"
currency: "USD"
contract_months: 12
discount_pct: 0.45
notes: "ExaCS BYOL — customer has existing Oracle DB EE licenses with active support"
line_items:
# ExaCS X11M — DB Servers (Queretaro Production)
- category: "Exadata Cloud Infrastructure"
product: "ExaCS X11M DB Server"
sku: "B110627"
qty: 2
unit: "Hosted Environment Per Hour"
hours_units: 744
months: 12
notes: "2 DB servers for Quarter Rack — Queretaro (Prod)"
# ExaCS X11M — Storage Servers (Queretaro Production)
- category: "Exadata Cloud Infrastructure"
product: "ExaCS X11M Storage Server"
sku: "B110629"
qty: 3
unit: "Hosted Environment Per Hour"
hours_units: 744
months: 12
notes: "3 storage servers — Quarter Rack minimum"
# ExaCS X11M — ECPU BYOL (Queretaro)
- category: "Exadata Cloud Infrastructure"
product: "ExaCS X11M ECPU BYOL"
sku: "B110632"
qty: 128
unit: "ECPU Per Hour"
hours_units: 744
months: 12
notes: "128 ECPUs BYOL — EBS + SAP + DW"
# ExaCS X11M — DB Servers (Sao Paulo DR Standby)
- category: "Exadata Cloud Infrastructure"
product: "ExaCS X11M DB Server (DR)"
sku: "B110627"
qty: 2
unit: "Hosted Environment Per Hour"
hours_units: 744
months: 12
notes: "2 DB servers — Sao Paulo (DR Standby)"
# ExaCS X11M — Storage Servers (Sao Paulo DR)
- category: "Exadata Cloud Infrastructure"
product: "ExaCS X11M Storage Server (DR)"
sku: "B110629"
qty: 3
unit: "Hosted Environment Per Hour"
hours_units: 744
months: 12
notes: "3 storage servers — DR Standby"
# ExaCS X11M — ECPU BYOL (Sao Paulo DR)
- category: "Exadata Cloud Infrastructure"
product: "ExaCS X11M ECPU BYOL (DR)"
sku: "B110632"
qty: 128
unit: "ECPU Per Hour"
hours_units: 744
months: 12
notes: "128 ECPUs BYOL — Data Guard standby"
# ADB-S for DW migration (Phase 2)
- category: "Autonomous Database"
product: "ADB-S ECPU BYOL"
sku: "B95703"
qty: 16
unit: "ECPU Per Hour"
hours_units: 744
months: 12
notes: "16 ECPU for DW migration to ADB-S (optional Phase 2)"
# ADB-S Storage
- category: "Autonomous Database"
product: "ADB-S Storage (ATP)"
sku: "B95706"
qty: 2000
unit: "Gigabyte Storage Capacity Per Month"
hours_units: 1
months: 12
notes: "2TB storage for DW"
# FastConnect 10Gbps Dual
- category: "Networking"
product: "FastConnect 10 Gbps (Primary)"
sku: "B88326"
qty: 1
unit: "Port Hour"
hours_units: 744
months: 12
notes: "Primary circuit — Mexico City to Queretaro"
- category: "Networking"
product: "FastConnect 10 Gbps (Redundant)"
sku: "B88326"
qty: 1
unit: "Port Hour"
hours_units: 744
months: 12
notes: "Redundant circuit — Mexico City to Queretaro"
# Object Storage for RMAN backups
- category: "Storage"
product: "Object Storage"
sku: "B91628"
qty: 50000
unit: "Gigabyte Storage Capacity Per Month"
hours_units: 1
months: 12
notes: "50TB for RMAN backups (DW + EBS)"
# Block Volume
- category: "Storage"
product: "Block Volume Storage"
sku: "B91961"
qty: 10000
unit: "Gigabyte Storage Capacity Per Month"
hours_units: 1
months: 12
notes: "10TB additional block storage"
# OCI Vault
- category: "Security"
product: "OCI Vault Key Versions"
sku: "B92092"
qty: 10
unit: "Key Version Per Month"
hours_units: 1
months: 12
notes: "TDE keys for 3 databases + DR + backups"

View File

@@ -1,168 +0,0 @@
# OCI Deal Accelerator — Business Case
# Demo: PharmaCorp Mexico — ExaCS migration from on-prem Exadata X6
# Follows exact schema expected by oci_bizcase_gen.py
business_case:
customer_name: "PharmaCorp Mexico"
prepared_by: "Carlos Mendoza — Solutions Architect"
date: "2026-04-12"
executive_summary: >
PharmaCorp Mexico operates 3 mission-critical Oracle databases (EBS R12, SAP, DW)
on aging Exadata X6 hardware. The hardware refresh quote is $1.8M and locks the
company into 5 more years of on-prem operations with no DR capability. Migration
to OCI ExaCS X11M reduces annual infrastructure costs by 61% ($1.75M over 3 years),
adds cross-region DR (RPO<15min, RTO<1h), and resolves the pending SOX audit
finding on automated patching.
drivers:
primary: "hardware_refresh_avoidance"
urgency: "HW refresh due in 6 months — must decide now or commit $1.8M"
cost_of_inaction:
financial: "$2.88M over 3yr: $1.8M HW refresh + $300K/yr support + $80K/yr DC costs"
operational: "No DR today — single site, single point of failure for EBS production"
strategic: "SOX audit finding pending: manual quarterly patching is non-compliant"
tco:
horizon_years: 3
current_state:
annual_infrastructure: 600000
annual_licensing: 220000
annual_operations: 60000
annual_downtime_cost: 50000
annual_compliance_cost: 30000
breakdown:
- item: "Exadata X6 depreciation + refresh amortization"
annual: 600000
- item: "Oracle DB EE Support & Maintenance"
annual: 220000
- item: "DBA patching + infra management (part-time)"
annual: 60000
- item: "Data center (power, cooling, space)"
annual: 80000
proposed_oci:
migration_one_time: 50000
annual_cloud_consumption: 278400
annual_licensing: 0
annual_operations: 20000
annual_downtime_cost: 0
annual_compliance_cost: 0
breakdown:
- item: "ExaCS X11M Queretaro (Prod, BYOL)"
annual: 98400
- item: "ExaCS X11M Sao Paulo (DR Standby, BYOL)"
annual: 98400
- item: "FastConnect 10Gbps Dual"
annual: 36720
- item: "OCI Vault + Monitoring + Storage"
annual: 30000
- item: "Reduced DBA operations (automated patching)"
annual: 20000
- item: "Migration project (one-time, amortized Y1)"
annual: 50000
savings:
annual: 661600
three_year: 1749600
pct: 61
roi:
three_year_roi_pct: 3400
payback_months: 1
total_investment: 50000
annual_net_benefit: 661600
notes: >
ROI based on 3-year TCO comparison. 1-month payback driven by immediate
elimination of HW refresh commitment ($1.8M avoided). Conservative — does
not include productivity gains from automated patching or DR risk reduction.
value_drivers:
- category: "Cost Avoidance"
driver: "Avoid $1.8M hardware refresh + eliminate DC costs"
quantification: "$1.75M saved over 3 years (61% reduction)"
- category: "Risk Reduction"
driver: "Cross-region DR where none exists today"
quantification: "RPO<15min, RTO<1h with Data Guard + FSDR"
- category: "Compliance"
driver: "SOX audit finding resolved with automated patching"
quantification: "ExaCS auto-patching + OCI Audit + Vault key management"
- category: "Operational Efficiency"
driver: "DBA freed from manual patching + infrastructure management"
quantification: "60% reduction in DBA time allocated to patching"
risks:
migration_risks:
- risk: "EBS R12 customizations may require OS-level changes post-migration"
probability: "Medium"
impact: "Medium"
mitigation: "Inventory all customizations pre-migration; test in Pre-Prod"
- risk: "FastConnect provisioning delay (partner-dependent)"
probability: "Medium"
impact: "Low"
mitigation: "Start in Phase 1 week 1; IPSec VPN as interim backup"
- risk: "HW refresh deadline pressure (6 months)"
probability: "High"
impact: "High"
mitigation: "16-week migration plan fits within window with margin"
do_nothing_risks:
- risk: "No DR — single site, single point of failure for EBS production"
probability: "High"
impact: "Critical"
mitigation: "Cannot be mitigated without infrastructure investment"
- risk: "SOX audit finding remains unresolved (manual patching)"
probability: "High"
impact: "High"
mitigation: "Remediation requires automated patching — on-prem can't deliver"
- risk: "$1.8M HW refresh locks in 5 more years of on-prem with no cloud ROI"
probability: "High"
impact: "High"
mitigation: "Sunk cost — delays any cloud migration benefit for 5+ years"
recommendation:
summary: >
Proceed with OCI ExaCS migration. 61% cost reduction over 3 years ($1.75M saved),
cross-region DR capability, and SOX compliance resolution. The alternative
(HW refresh at $1.8M) costs more and leaves DR + compliance gaps unresolved.
next_steps:
- "Board approval with TCO presentation — May 2026"
- "Provision OCI tenancy + ExaCS in Queretaro — June 2026"
- "Start FastConnect provisioning with partner — June 2026"
- "EBS customization inventory and Pre-Prod testing — June 2026"
- "Production cutover — September 2026"
roadmap:
total_duration: "16 weeks (May — September 2026)"
phases:
- name: "Board Approval"
duration: "2 weeks"
investment: 0
description: "Present TCO + risk analysis to board"
- name: "Foundation (networking + ExaCS)"
duration: "4 weeks"
investment: 15000
description: "Provision OCI tenancy, ExaCS, FastConnect"
- name: "EBS Migration + Data Guard"
duration: "6 weeks"
investment: 25000
description: "Migrate EBS R12 with ZDM, configure Data Guard to Sao Paulo"
- name: "SAP + DW Migration"
duration: "4 weeks"
investment: 10000
description: "Migrate remaining databases, RMAN backup for DW"
- name: "Cutover + Decommission"
duration: "2 weeks"
investment: 0
description: "Production cutover, decommission Exadata X6"

View File

@@ -1,129 +0,0 @@
# OCI Deal Accelerator — Architecture Diagram Spec
# Demo: PharmaCorp Mexico — ExaCS dual-region with Data Guard
# Prompt: "ExaCS en Queretaro (prod) + Standby con Data Guard en Sao Paulo,
# Hub-Spoke networking con DRG, FastConnect 10Gbps dual desde Mexico City,
# bastion en public subnet, WAF para apps web, Vault para TDE keys."
#
# REQUESTED by user: ExaCS (prod+DR), DRG, FastConnect dual, Bastion, WAF, Vault
# TECHNICAL DEPENDENCIES (whitelist): SGW (ExaCS backup), IGW (public subnet)
# NOT included: EBS, Monitoring, Logging, Events, FSDR, Object Storage (not requested)
#
# Container sizes: omitted to let auto-sizing calculate from content.
# DRG: listed in gateways — generator places it OUTSIDE VCN, INSIDE region.
title: "PharmaCorp Mexico — ExaCS Migration Architecture"
# External: Mexico City DC (left side)
external:
- id: "mexico_dc"
label: "Mexico City\nData Center"
icon: "user"
x: 30
y: 250
w: 60
h: 70
# OCI Tenancy
tenancy:
label: "Oracle Cloud Infrastructure"
x: 200
y: 60
regions:
# ── Primary Region: Queretaro ──
- id: "region_qro"
label: "Region — Queretaro (Primary)"
primary: true
vcns:
- id: "vcn_hub"
label: "Hub VCN (10.0.0.0/16)"
subnets:
# Public subnet — WAF + Bastion (both requested)
- id: "subnet_pub"
label: "Public Subnet — 10.0.0.0/24"
h: 120
services:
- id: "waf"
label: "WAF\n(Web Apps)"
type: "waf"
w: 120
h: 70
- id: "bastion"
label: "Bastion\nService"
type: "bastion"
w: 120
h: 70
# DB subnet — ExaCS + Vault (both requested)
- id: "subnet_db"
label: "Database Subnet (Private) — 10.0.1.0/24"
h: 130
services:
- id: "exacs_prod"
label: "ExaCS X11M\nQuarter Rack\n(Production)"
type: "exacs"
w: 200
h: 80
- id: "vault"
label: "OCI Vault\nTDE Keys"
type: "vault"
w: 140
h: 80
gateways:
# DRG — generator places this OUTSIDE VCN, INSIDE region
- id: "drg"
label: "DRG"
type: "drg"
# IGW — whitelist: public subnet requires it
- id: "igw"
label: "Internet\nGateway"
type: "igw"
# SGW — whitelist: ExaCS backup requires it
- id: "sgw"
label: "Service\nGateway"
type: "service_gateway"
# ── DR Region: Sao Paulo ──
- id: "region_gru"
label: "Region — São Paulo (DR)"
vcns:
- id: "vcn_dr"
label: "DR VCN (10.1.0.0/16)"
subnets:
- id: "subnet_dr_db"
label: "DB Subnet (Private) — 10.1.1.0/24"
h: 130
services:
- id: "exacs_dr"
label: "ExaCS Standby\n(Data Guard)"
type: "exacs"
w: 200
h: 80
# Connections
connections:
# FastConnect dual from Mexico City → DRG (will be merged into single edge)
- id: "conn_fc1"
label: "FastConnect 10Gbps\n(Dual Redundant)"
type: "fastconnect"
from: "mexico_dc"
to: "drg"
# Vault → ExaCS (TDE keys)
- id: "conn_vault"
label: "TDE Keys"
type: "internal"
from: "vault"
to: "exacs_prod"
# Data Guard: Prod → DR
- id: "conn_dg"
label: "Data Guard\n(Redo Shipping)"
type: "data"
from: "exacs_prod"
to: "exacs_dr"

View File

@@ -1,111 +0,0 @@
{
"cost": {
"line_items": [
{
"component": "ExaCS X11M Quarter Rack \u2014 Queretaro (Prod)",
"monthly_payg": "$18,500",
"monthly_byol": "$8,200",
"notes": "2 DB servers, 3 storage servers, BYOL"
},
{
"component": "ExaCS X11M Quarter Rack \u2014 Sao Paulo (DR)",
"monthly_payg": "$18,500",
"monthly_byol": "$8,200",
"notes": "ADG standby, BYOL"
},
{
"component": "ADB-S 16 ECPU (DW alternative \u2014 future)",
"monthly_payg": "$2,400",
"monthly_byol": "$1,200",
"notes": "Optional \u2014 migrate DW to ADB-S in Phase 2"
},
{
"component": "FastConnect 10Gbps Dual",
"monthly_payg": "$3,060",
"notes": "2 x $1,530/month"
},
{
"component": "OCI Vault",
"monthly_payg": "$500",
"notes": "HSM-backed key management"
},
{
"component": "Object Storage (50TB backup)",
"monthly_payg": "$1,200",
"notes": "RMAN backups + DW backup"
},
{
"component": "WAF + Logging + Monitoring",
"monthly_payg": "$800",
"notes": "WAF for EBS web tier + audit logging"
},
{
"component": "TOTAL",
"monthly_payg": "$45,000",
"monthly_byol": "$23,200",
"notes": "BYOL saves 48%"
}
],
"assumptions": [
"BYOL assumes existing Oracle Database EE licenses with active support",
"ExaCS pricing based on current OCI list prices",
"DR standby at full rate (ADG requires active compute)",
"No middleware licensing included (EBS app tier on compute VMs)",
"FastConnect partner charges not included"
],
"show_byol": true
},
"cost_comparison": {
"title": "Cost Comparison: On-Prem HW Refresh vs OCI Migration",
"col_headers": [
"Component",
"On-Prem Refresh (5yr)",
"OCI ExaCS BYOL",
"Delta"
],
"rows": [
{
"item": "Infrastructure (hardware/cloud)",
"current": "$1,800,000 upfront",
"oci": "$278,400/yr",
"savings": "Break-even at 3.2 years"
},
{
"item": "Annual Support & Maintenance",
"current": "$220,000/yr",
"oci": "Included in OCI",
"savings": "-100%"
},
{
"item": "DR Infrastructure",
"current": "$0 (no DR today)",
"oci": "$98,400/yr",
"savings": "New capability \u2014 no DR today"
},
{
"item": "Data Center (power/cooling/space)",
"current": "$80,000/yr",
"oci": "$0",
"savings": "-100%"
},
{
"item": "DBA Operations (patching)",
"current": "$60,000/yr (part-time DBA)",
"oci": "$0 (automated)",
"savings": "-100%"
},
{
"item": "TOTAL ANNUAL (year 1)",
"current": "$2,160,000",
"oci": "$376,800",
"savings": "-83%"
},
{
"item": "TOTAL 3-YEAR TCO",
"current": "$2,880,000",
"oci": "$1,130,400",
"savings": "-$1.75M (61%)"
}
]
}
}

View File

@@ -1,348 +0,0 @@
well_architected_scorecard:
overall_status: GAPS_IDENTIFIED
generated_date: '2026-04-12T23:15:18.437115'
architecture_name: /tmp/demo-flawed-arch.yaml
customer: Demo Flawed Arch
summary:
total_checks: 23
total_passed: 1
total_gaps: 22
high_severity_gaps: 10
medium_severity_gaps: 10
low_severity_gaps: 2
pillars:
security_compliance:
status: GAPS_IDENTIFIED
checks_passed: 0
checks_total: 15
categories:
identity_access:
passed: 0
total: 4
gaps:
- check_id: SEC-IAM-001
area: Identity & Access
finding: no IAM policy strategy defined or broad permissions used
severity: HIGH
recommendation: architecture specifies granular IAM policies per compartment/service
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/effective-strategies-security-and-compliance1.html
- check_id: SEC-IAM-002
area: Identity & Access
finding: no MFA mentioned in identity setup
severity: HIGH
recommendation: MFA is specified in identity configuration
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/effective-strategies-security-and-compliance1.html
- check_id: SEC-IAM-003
area: Identity & Access
finding: no mention of admin account restrictions
severity: HIGH
recommendation: separate admin groups defined for operations
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/effective-strategies-security-and-compliance1.html
- check_id: SEC-IAM-004
area: Identity & Access
finding: stored credentials or API keys used for service accounts
severity: HIGH
recommendation: instance principals or resource principals specified for
service-to-service auth
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/effective-strategies-security-and-compliance1.html
resource_isolation:
passed: 0
total: 3
gaps:
- check_id: SEC-ISO-001
area: Resource Isolation
finding: flat compartment structure or no compartment strategy
severity: HIGH
recommendation: hierarchical compartment structure defined
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/effective-strategies-security-and-compliance1.html
- check_id: SEC-ISO-002
area: Resource Isolation
finding: no tagging strategy defined
severity: MEDIUM
recommendation: defined tag namespaces with cost, environment, and owner
tags
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/effective-strategies-security-and-compliance1.html
- check_id: SEC-ISO-004
area: Resource Isolation
finding: user credentials used for cross-resource access
severity: HIGH
recommendation: resource principals used for cross-service access
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/effective-strategies-security-and-compliance1.html
database_security:
passed: 0
total: 0
gaps: []
data_protection:
passed: 0
total: 2
gaps:
- check_id: SEC-DAT-001
area: Data Protection
finding: any storage service without encryption at rest
severity: HIGH
recommendation: encryption at rest specified for all storage services
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/effective-strategies-security-and-compliance1.html
- check_id: SEC-DAT-002
area: Data Protection
finding: using Oracle-managed keys or no key management strategy
severity: MEDIUM
recommendation: OCI Vault specified for key management
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/effective-strategies-security-and-compliance1.html
network_security:
passed: 0
total: 3
gaps:
- check_id: SEC-NET-001
area: Network Security
finding: only Security Lists used without NSGs
severity: MEDIUM
recommendation: NSGs used as primary network security mechanism
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/effective-strategies-security-and-compliance1.html
- check_id: SEC-NET-002
area: Network Security
finding: SSH open to 0.0.0.0/0
severity: HIGH
recommendation: SSH access restricted to specific CIDR ranges or bastion
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/effective-strategies-security-and-compliance1.html
- check_id: SEC-NET-004
area: Network Security
finding: OCI services accessed via internet gateway
severity: MEDIUM
recommendation: Service Gateway configured in VCN
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/effective-strategies-security-and-compliance1.html
monitoring_audit:
passed: 0
total: 3
gaps:
- check_id: SEC-MON-001
area: Monitoring & Audit
finding: Cloud Guard not included in architecture
severity: HIGH
recommendation: Cloud Guard configured with appropriate recipes
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/effective-strategies-security-and-compliance1.html
- check_id: SEC-MON-002
area: Monitoring & Audit
finding: audit service not mentioned or retention not configured
severity: MEDIUM
recommendation: Audit service enabled (enabled by default, verify retention)
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/effective-strategies-security-and-compliance1.html
- check_id: SEC-MON-003
area: Monitoring & Audit
finding: no VCN Flow Logs configured
severity: MEDIUM
recommendation: VCN Flow Logs enabled for critical subnets
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/effective-strategies-security-and-compliance1.html
reliability_resilience:
status: PASS_WITH_RECOMMENDATIONS
checks_passed: 0
checks_total: 1
categories:
scalability:
passed: 0
total: 1
gaps:
- check_id: REL-SCL-002
area: Scalability
finding: no service limit review planned
severity: LOW
recommendation: service limit review mentioned in deployment plan
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/reliable-and-resilient-cloud-topology-practices1.html
fault_tolerant_networking:
passed: 0
total: 0
gaps: []
data_backup:
passed: 0
total: 0
gaps: []
data_replication:
passed: 0
total: 0
gaps: []
disaster_recovery:
passed: 0
total: 0
gaps: []
performance_cost:
status: PASS_WITH_RECOMMENDATIONS
checks_passed: 0
checks_total: 2
categories:
compute_sizing:
passed: 0
total: 0
gaps: []
storage_strategy:
passed: 0
total: 0
gaps: []
network_tuning:
passed: 0
total: 0
gaps: []
cost_management:
passed: 0
total: 2
gaps:
- check_id: PERF-CST-003
area: Cost Management
finding: no budget alerts configured
severity: MEDIUM
recommendation: OCI Budgets configured with alert thresholds
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/index.html
- check_id: PERF-CST-004
area: Cost Management
finding: no cost attribution tagging strategy
severity: MEDIUM
recommendation: cost-tracking tags defined and enforced
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/index.html
operational_efficiency:
status: GAPS_IDENTIFIED
checks_passed: 1
checks_total: 5
categories:
deployment_strategy:
passed: 0
total: 1
gaps:
- check_id: OPS-DEP-001
area: Deployment Strategy
finding: no IaC strategy specified
severity: HIGH
recommendation: Terraform or Resource Manager specified for infrastructure
provisioning
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/best-practices-operating-cloud-deployments-efficiency.html
workload_monitoring:
passed: 1
total: 2
gaps:
- check_id: OPS-MON-003
area: Workload Monitoring
finding: no centralized log analysis solution
severity: MEDIUM
recommendation: OCI Logging Analytics configured
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/best-practices-operating-cloud-deployments-efficiency.html
os_management:
passed: 0
total: 0
gaps: []
operations_support:
passed: 0
total: 2
gaps:
- check_id: OPS-SUP-001
area: Operations Support
finding: no support plan consideration
severity: LOW
recommendation: Oracle support plan level specified and matches criticality
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/best-practices-operating-cloud-deployments-efficiency.html
- check_id: OPS-SUP-002
area: Operations Support
finding: no notification configuration
severity: MEDIUM
recommendation: OCI Notifications configured for operational events
wa_reference: https://docs.oracle.com/en/solutions/oci-best-practices/best-practices-operating-cloud-deployments-efficiency.html
distributed_cloud:
status: NOT_APPLICABLE
checks_passed: 0
checks_total: 0
categories: {}
action_items:
high_priority:
- check_id: SEC-IAM-001
pillar: Identity & Access
finding: no IAM policy strategy defined or broad permissions used
recommendation: architecture specifies granular IAM policies per compartment/service
- check_id: SEC-IAM-002
pillar: Identity & Access
finding: no MFA mentioned in identity setup
recommendation: MFA is specified in identity configuration
- check_id: SEC-IAM-003
pillar: Identity & Access
finding: no mention of admin account restrictions
recommendation: separate admin groups defined for operations
- check_id: SEC-IAM-004
pillar: Identity & Access
finding: stored credentials or API keys used for service accounts
recommendation: instance principals or resource principals specified for service-to-service
auth
- check_id: SEC-ISO-001
pillar: Resource Isolation
finding: flat compartment structure or no compartment strategy
recommendation: hierarchical compartment structure defined
- check_id: SEC-ISO-004
pillar: Resource Isolation
finding: user credentials used for cross-resource access
recommendation: resource principals used for cross-service access
- check_id: SEC-DAT-001
pillar: Data Protection
finding: any storage service without encryption at rest
recommendation: encryption at rest specified for all storage services
- check_id: SEC-NET-002
pillar: Network Security
finding: SSH open to 0.0.0.0/0
recommendation: SSH access restricted to specific CIDR ranges or bastion
- check_id: SEC-MON-001
pillar: Monitoring & Audit
finding: Cloud Guard not included in architecture
recommendation: Cloud Guard configured with appropriate recipes
- check_id: OPS-DEP-001
pillar: Deployment Strategy
finding: no IaC strategy specified
recommendation: Terraform or Resource Manager specified for infrastructure provisioning
medium_priority:
- check_id: SEC-ISO-002
pillar: Resource Isolation
finding: no tagging strategy defined
recommendation: defined tag namespaces with cost, environment, and owner tags
- check_id: SEC-DAT-002
pillar: Data Protection
finding: using Oracle-managed keys or no key management strategy
recommendation: OCI Vault specified for key management
- check_id: SEC-NET-001
pillar: Network Security
finding: only Security Lists used without NSGs
recommendation: NSGs used as primary network security mechanism
- check_id: SEC-NET-004
pillar: Network Security
finding: OCI services accessed via internet gateway
recommendation: Service Gateway configured in VCN
- check_id: SEC-MON-002
pillar: Monitoring & Audit
finding: audit service not mentioned or retention not configured
recommendation: Audit service enabled (enabled by default, verify retention)
- check_id: SEC-MON-003
pillar: Monitoring & Audit
finding: no VCN Flow Logs configured
recommendation: VCN Flow Logs enabled for critical subnets
- check_id: PERF-CST-003
pillar: Cost Management
finding: no budget alerts configured
recommendation: OCI Budgets configured with alert thresholds
- check_id: PERF-CST-004
pillar: Cost Management
finding: no cost attribution tagging strategy
recommendation: cost-tracking tags defined and enforced
- check_id: OPS-MON-003
pillar: Workload Monitoring
finding: no centralized log analysis solution
recommendation: OCI Logging Analytics configured
- check_id: OPS-SUP-002
pillar: Operations Support
finding: no notification configuration
recommendation: OCI Notifications configured for operational events
low_priority:
- check_id: REL-SCL-002
pillar: Scalability
finding: no service limit review planned
recommendation: service limit review mentioned in deployment plan
- check_id: OPS-SUP-001
pillar: Operations Support
finding: no support plan consideration
recommendation: Oracle support plan level specified and matches criticality
references:
framework: https://docs.oracle.com/en/solutions/oci-best-practices/index.html
security: https://docs.oracle.com/en/solutions/oci-best-practices/effective-strategies-security-and-compliance1.html
reliability: https://docs.oracle.com/en/solutions/oci-best-practices/reliable-and-resilient-cloud-topology-practices1.html
operations: https://docs.oracle.com/en/solutions/oci-best-practices/best-practices-operating-cloud-deployments-efficiency.html
distributed: https://docs.oracle.com/en/solutions/oci-best-practices/effective-strategies-distributed-cloud-implementation1.html

View File

@@ -1,92 +0,0 @@
[
{
"feature": "Auto Indexing",
"category": "auto_management",
"compatibility": [
{
"deployment": "ADB-S Serverless",
"version": "23ai",
"status": "GA_CAVEAT",
"notes": "Creates indexes HIDDEN by default. Instantly reversible. DML costing prevents over-indexing. Deduplication with manually created indexes."
},
{
"deployment": "ADB-S Serverless",
"version": "26ai",
"status": "GA",
"notes": "Improved in 26ai \u2014 better cost model, fewer false positives."
},
{
"deployment": "ADB-S Elastic Pool",
"version": "23ai",
"status": "GA_CAVEAT",
"notes": "Same as ADB-S Serverless."
},
{
"deployment": "ADB Dedicated Elastic Pool (DEP)",
"version": "23ai",
"status": "GA_CAVEAT",
"notes": "Available but less tested in DEP."
},
{
"deployment": "ADB on Dedicated Exadata",
"version": "23ai",
"status": "GA",
"notes": "Fully supported."
},
{
"deployment": "Exadata Cloud Service",
"version": "23ai",
"status": "NOT_AVAIL",
"notes": "Not an ADB feature. Use manual indexing."
},
{
"deployment": "Base DB Service (EE)",
"version": "23ai",
"status": "NOT_AVAIL",
"notes": ""
}
]
},
{
"feature": "AI Vector Search (HNSW)",
"category": "developer",
"compatibility": [
{
"deployment": "ADB-S Serverless",
"version": "23ai",
"status": "GA_CAVEAT",
"notes": "HNSW indexes are node-local on multi-node RAC. Works well up to\n~50M vectors on single node (\u226424 ECPUs). At 100M+ on multi-node\n(\u226564 ECPUs), requires hash-partitioned vector table with LOCAL\nHNSW index for <50ms P95. See FF-202603-008.\n"
},
{
"deployment": "ADB-S Serverless",
"version": "26ai",
"status": "UNTESTED",
"notes": "Check if distributed HNSW is added in 26ai. Expected to remain node-local."
},
{
"deployment": "ADB-S Elastic Pool",
"version": "23ai",
"status": "GA_CAVEAT",
"notes": "Same node-local limitation as ADB-S Serverless."
},
{
"deployment": "ADB on Dedicated Exadata",
"version": "23ai",
"status": "GA_CAVEAT",
"notes": "Same limitation. Dedicated infra doesn't change HNSW locality."
},
{
"deployment": "Exadata Cloud Service",
"version": "23ai",
"status": "GA_CAVEAT",
"notes": "RAC with HNSW has same node-local constraint. Partitioning workaround applies."
},
{
"deployment": "Base DB Service (EE)",
"version": "23ai",
"status": "NOT_AVAIL",
"notes": "AI Vector Search requires 23ai. Available in EE but HNSW performance limited without Exadata storage."
}
]
}
]

View File

@@ -1,25 +0,0 @@
{
"query": "ADB-S vs AWS Aurora PostgreSQL",
"aws_database_mappings": [],
"relevant_objections": [
{
"id": "OBJ-001",
"category": "market",
"objection": "OCI is a niche cloud \u2014 AWS/Azure are the leaders",
"response": "OCI is the #4 cloud globally and growing fastest in database PaaS.\nFor Oracle database workloads specifically, OCI is the leader:\n- Only cloud with Exadata hardware\n- Only cloud with Autonomous Database\n- Best Oracle license portability\nThe question isn't \"best cloud overall\" but \"best cloud for this workload.\"\n",
"evidence": "Gartner, IDC reports on cloud database market"
},
{
"id": "OBJ-004",
"category": "features",
"objection": "OCI doesn't have as many services as AWS",
"response": "True for breadth (AWS has 200+ services). For database and enterprise\nworkloads, OCI feature parity is strong. Key services are GA:\n- Compute, networking, storage, Kubernetes, serverless\n- Monitoring, logging, IAM, security\n- Queue, Streaming, API Gateway\nWhat matters is whether OCI has what *you* need, not a raw count.\n"
},
{
"id": "OBJ-005",
"category": "cost",
"objection": "We can get better pricing from AWS/Azure",
"response": "OCI list prices are already 30-50% below AWS for equivalent configs.\nFor Oracle specifically:\n- No Oracle license surcharge on OCI\n- BYOL discount is deeper on OCI\n- Data egress is 10x cheaper\nRequest a detailed cost comparison with matched configurations.\n"
}
]
}

View File

@@ -1,121 +0,0 @@
[
{
"id": "FF-202603-002",
"date": "2026-03-08",
"contributor": {
"name": "Diego Cabrera",
"team": "Field Architecture",
"client": "Strategic Migration Customer",
"context": "Production migration",
"confidence": "validated"
},
"product": "ADB-S Dedicated Elastic Pool (DEP)",
"version": "23ai",
"severity": "HIGH",
"category": "limitation",
"summary": "ADG must be disabled before joining Dedicated Elastic Pool",
"detail": "When attempting to add an ADB-S instance with active Autonomous Data Guard\nto a Dedicated Elastic Pool, the operation fails. ADG must be explicitly\ndisabled before the instance can join the DEP. This is a known issue\nacknowledged by Oracle but not prominently documented.\n\nImpact: requires a brief HA gap during DEP onboarding. Must coordinate\nwith the customer's change window.\n",
"workaround": "Disable ADG \u2192 join DEP \u2192 re-enable ADG. Plan for ~15 min HA gap.",
"oracle_sr": "",
"status": "acknowledged",
"resolved_date": null,
"resolution": null,
"affects_matrix": "Elastic Pool Membership",
"tags": [
"dep",
"adg",
"elastic-pool",
"ha",
"limitation"
]
},
{
"id": "FF-202603-003",
"date": "2026-03-05",
"contributor": {
"name": "Diego Cabrera",
"team": "Field Architecture",
"client": "Strategic Migration Customer",
"context": "Production migration",
"confidence": "validated"
},
"product": "ADB-S Dedicated Elastic Pool (DEP)",
"version": "23ai",
"severity": "MEDIUM",
"category": "undocumented",
"summary": "DEP maintenance window immutable post-creation \u2014 change only via SR",
"detail": "The maintenance window for a Dedicated Elastic Pool cannot be changed\nafter creation via Console, CLI, or API. The only way to change it is\nby filing a Service Request with Oracle Support.\n\nDefault window is Saturday/Sunday. Other days require specifying at\ncreation time (which itself has the CLI bug \u2014 see FF-202603-001) or\nrequesting via SR.\n",
"workaround": "Set desired window at DEP creation time (via Console, not CLI). If missed, file SR.",
"oracle_sr": "",
"status": "acknowledged",
"resolved_date": null,
"resolution": null,
"affects_matrix": "Auto Patching",
"tags": [
"dep",
"maintenance-window",
"immutable",
"sr-required"
]
},
{
"id": "FF-202603-004",
"date": "2026-03-01",
"contributor": {
"name": "Diego Cabrera",
"team": "Field Architecture",
"client": "Strategic Migration Customer",
"context": "Production migration",
"confidence": "validated"
},
"product": "ADB-S",
"version": "23ai",
"severity": "MEDIUM",
"category": "gotcha",
"summary": "DEP provisioning takes days to weeks depending on capacity",
"detail": "Dedicated Elastic Pool provisioning is NOT instant like ADB-S Serverless.\nIt requires physical Exadata infrastructure allocation. Lead time varies\nfrom 3 days (if capacity available in region) to 2-3 weeks (if capacity\nneeds to be provisioned).\n\nBilling starts only at AVAILABLE state, not at request time.\nCo-location of existing ADB-S instances happens at the next maintenance\nwindow after joining, not immediately.\n",
"workaround": "Request DEP early in the project timeline. Don't make it a critical-path dependency in week 1.",
"oracle_sr": "",
"status": "acknowledged",
"resolved_date": null,
"resolution": null,
"affects_matrix": "Elastic Pool Membership",
"tags": [
"dep",
"provisioning",
"lead-time",
"capacity",
"planning"
]
},
{
"id": "FF-202603-005",
"date": "2026-02-20",
"contributor": {
"name": "Diego Cabrera",
"team": "Field Architecture",
"client": "Strategic Migration Customer",
"context": "Production migration",
"confidence": "observed"
},
"product": "ADB-S",
"version": "23ai",
"severity": "LOW",
"category": "gotcha",
"summary": "Refreshable Clone failed refresh leaves clone in AVAILABLE state \u2014 detection delay",
"detail": "When a Refreshable Clone's auto-refresh fails, the clone remains in\nAVAILABLE state (not DISCONNECTED). The Elastic Pool leader monitors\nlifecycle state changes, not refresh success/failure.\n\nThis means a failed refresh is NOT immediately detected by the\ninfrastructure. The stale data window equals the ADG RTO for\nunplanned scenarios.\n",
"workaround": "Planned switchovers: DBMS_PROXY_SQL.DISABLE_READ_ONLY_OFFLOAD proactively.\nUnplanned: OCI Events + OCI Function that monitors refresh state and\ndisables read offload automatically. Reduces stale window to ~ADG RTO.\n",
"oracle_sr": "",
"status": "monitoring",
"resolved_date": null,
"resolution": null,
"affects_matrix": "Refreshable Clone",
"tags": [
"refreshable-clone",
"stale-reads",
"elastic-pool",
"detection-delay",
"proxysql"
]
}
]

View File

@@ -1,300 +0,0 @@
[
{
"title": "Oracle MAA for Oracle Database@Azure",
"url": "https://docs.oracle.com/en/solutions/oracle-maa-db-at-azure/index.html",
"type": "reference-architecture",
"date": "2025-05",
"services": [
"exacs",
"adg",
"vault",
"azure"
],
"tags": [
"database",
"multicloud",
"azure",
"ha-dr",
"autonomous"
],
"summary": "Cross-AZ Data Guard on ExaCS in Database@Azure. Active Data Guard recommended for cross-AZ replication (block repair, app continuity, read offload). Backups to Autonomous Recovery Service.\n",
"terraform": null
},
{
"title": "Implement disaster recovery with local and regional standbys on Oracle Database@Azure",
"url": "https://docs.oracle.com/en/solutions/local-regional-standby-dr-db-at-azure/index.html",
"type": "reference-architecture",
"date": "2025-05",
"services": [
"exacs",
"adg",
"azure",
"fsdr"
],
"tags": [
"database",
"multicloud",
"azure",
"ha-dr"
],
"summary": "Local standby for HA within same region, regional standby for DR across Azure regions. Uses Active Data Guard with Fast-Start Failover. Recommends Full Stack DR service for automated failover orchestration.\n",
"terraform": null
},
{
"title": "Implement disaster recovery with multi-region standby on Oracle Database@Azure",
"url": "https://docs.oracle.com/en/solutions/multi-region-standby-dr-db-at-azure/index.html",
"type": "reference-architecture",
"date": "2025-04",
"services": [
"exacs",
"adg",
"azure"
],
"tags": [
"database",
"multicloud",
"azure",
"ha-dr"
],
"summary": "Multi-region DR for ExaCS on Database@Azure using Data Guard. Cross-region standby with switchover/failover capabilities. Includes network topology for Azure-to-Azure cross-region connectivity.\n",
"terraform": null
},
{
"title": "Implement cross-region disaster recovery for Exadata Database on Oracle Database@Azure",
"url": "https://docs.oracle.com/en/solutions/exadb-dr-on-db-azure/index.html",
"type": "reference-architecture",
"date": "2025-01",
"services": [
"exacs",
"adg",
"azure",
"fsdr"
],
"tags": [
"database",
"multicloud",
"azure",
"ha-dr"
],
"summary": "Cross-region DR for ExaCS on Database@Azure. OCI-managed networks for peering (better performance, first 10 TB/month free). Data Guard for cross-region replication, FSDR for automated failover orchestration.\n",
"terraform": "https://github.com/oracle-quickstart/oci-multicloud-azure"
},
{
"title": "Deploy Active Data Guard Far Sync to protect data across Oracle Database@Azure regions",
"url": "https://docs.oracle.com/en/solutions/active-data-guard-far-sync-oracle-db-at-azure/index.html",
"type": "reference-architecture",
"date": "2025-02",
"services": [
"exacs",
"adg",
"azure"
],
"tags": [
"database",
"multicloud",
"azure",
"ha-dr"
],
"summary": "Far Sync instance for zero data loss protection across Azure regions. Synchronous redo transport to Far Sync, async to remote standby. Eliminates performance impact of synchronous shipping over long distances.\n",
"terraform": null
},
{
"title": "Implement Oracle Database Autonomous Recovery Service with Oracle Database@Azure",
"url": "https://docs.oracle.com/en/solutions/implement-recovery-service-db-at-azure/index.html",
"type": "reference-architecture",
"date": "2025-01",
"services": [
"exacs",
"azure"
],
"tags": [
"database",
"multicloud",
"azure",
"ha-dr"
],
"summary": "Autonomous Recovery Service for ExaCS backups on Database@Azure. Automated backup management with real-time protection and point-in-time recovery capabilities.\n",
"terraform": null
},
{
"title": "Deploy Active Data Guard Far Sync to protect data across Oracle Database@AWS regions",
"url": "https://docs.oracle.com/en/solutions/dr-active-dg-farsync-db-at-aws/index.html",
"type": "reference-architecture",
"date": "2025-09",
"services": [
"exacs",
"adg",
"aws"
],
"tags": [
"database",
"multicloud",
"aws",
"ha-dr"
],
"summary": "Far Sync instance for zero data loss protection across AWS regions. Synchronous redo to local Far Sync, asynchronous to remote standby. Requires cross-region AWS networking for redo transport.\n",
"terraform": null
},
{
"title": "Implement disaster recovery with local and regional standbys on Oracle Database@AWS",
"url": "https://docs.oracle.com/en/solutions/dr-local-regional-standby-db-at-aws/index.html",
"type": "reference-architecture",
"date": "2025-09",
"services": [
"exacs",
"adg",
"aws",
"fsdr"
],
"tags": [
"database",
"multicloud",
"aws",
"ha-dr"
],
"summary": "Local standby for HA within same AWS region, regional standby for DR. Active Data Guard with Fast-Start Failover for automated failover. Full Stack DR service for cross-service failover orchestration.\n",
"terraform": null
},
{
"title": "Implement disaster recovery with cross-zonal Data Guard on Oracle Database@AWS",
"url": "https://docs.oracle.com/en/solutions/cross-zone-dr-db-at-aws/index.html",
"type": "reference-architecture",
"date": "2025-08",
"services": [
"exacs",
"adg",
"aws"
],
"tags": [
"database",
"multicloud",
"aws",
"ha-dr"
],
"summary": "Cross-AZ Data Guard on Database@AWS for HA. Synchronous redo transport between AZs for zero data loss. Fast-Start Failover for automatic database switchover on failure.\n",
"terraform": null
},
{
"title": "Implement disaster recovery with cross-regional Data Guard on Oracle Database@AWS",
"url": "https://docs.oracle.com/en/solutions/implement-dr-cross-region-dg-odb-aws/index.html",
"type": "reference-architecture",
"date": "2025-08",
"services": [
"exacs",
"adg",
"aws"
],
"tags": [
"database",
"multicloud",
"aws",
"ha-dr"
],
"summary": "Cross-region Data Guard on Database@AWS for DR. Asynchronous redo transport for cross-region replication. Planned switchover for maintenance, forced failover for disaster scenarios.\n",
"terraform": null
},
{
"title": "Migrate Oracle RAC Databases to OCI",
"url": "https://docs.oracle.com/en/solutions/ensure-ha-migrate-vmware-workloads-to-oci/migrate-oracle-rac-databases1.html",
"type": "reference-architecture",
"date": "2025-01",
"services": [
"exacs",
"exascale",
"adb-d",
"base-db"
],
"tags": [
"database",
"migration",
"ha-dr"
],
"summary": "Compares 4 RAC migration targets: ExaCS Dedicated, Exascale, ADB-D, and 2-node RAC on Base DB. ExaCS recommended for full RAC feature parity. Exascale for 23ai-only without dedicated infra commitment.\n",
"terraform": null
},
{
"title": "Configure Data Guard for Oracle Exadata Database Service on Dedicated Infrastructure",
"url": "https://docs.oracle.com/en/solutions/dataguard-exadata-dedicated-infrastructure/index.html",
"type": "reference-architecture",
"date": "2025-07",
"services": [
"exacs",
"adg"
],
"tags": [
"database",
"ha-dr"
],
"summary": "Data Guard configuration for ExaCS Dedicated. Active Data Guard for read offload and automatic failover. Cross-region standby for DR with Fast-Start Failover for automated switchover.\n",
"terraform": null
},
{
"title": "Back up from Exadata on Dedicated Infrastructure to Zero Data Loss Autonomous Recovery",
"url": "https://docs.oracle.com/en/solutions/back-up-exadata-dedicated-zero-data-loss-recovery/index.html",
"type": "reference-architecture",
"date": "2025-07",
"services": [
"exacs"
],
"tags": [
"database",
"ha-dr"
],
"summary": "Autonomous Recovery Service for ExaCS backups. Automated backup management with real-time redo protection. Validates backup recoverability automatically.\n",
"terraform": null
},
{
"title": "Configure a standby database for disaster recovery using Oracle Exadata Database Service on Cloud@Customer",
"url": "https://docs.oracle.com/en/solutions/exadata-cloud-at-customer-standby/index.html",
"type": "reference-architecture",
"date": "2025-07",
"services": [
"exacs",
"adg"
],
"tags": [
"database",
"ha-dr"
],
"summary": "Data Guard standby on ExaCS Cloud@Customer for hybrid DR. On-premises primary with cloud standby, or cloud primary with on-premises standby for data sovereignty requirements.\n",
"terraform": null
},
{
"title": "Learn about maximum availability architecture for PeopleSoft",
"url": "https://docs.oracle.com/en/solutions/learn-about-maa-for-peoplesoft/index.html",
"type": "reference-architecture",
"date": "2024-08",
"services": [
"exacs",
"adg",
"load-balancer",
"compute"
],
"tags": [
"application",
"ha-dr",
"peoplesoft"
],
"summary": "MAA for PeopleSoft on OCI. Active Data Guard for database HA/DR, multi-instance PIA behind Load Balancer, Process Scheduler on multiple nodes for batch processing resilience.\n",
"terraform": null
},
{
"title": "Provision and deploy a maximum availability solution for PeopleSoft on Oracle Cloud",
"url": "https://docs.oracle.com/en/solutions/deploy-maa-for-peoplesoft-on-oci/index.html",
"type": "reference-architecture",
"date": "2024-08",
"services": [
"exacs",
"adg",
"load-balancer",
"compute"
],
"tags": [
"application",
"ha-dr",
"peoplesoft"
],
"summary": "Step-by-step PeopleSoft MAA deployment on OCI. Terraform stacks for infrastructure provisioning, Data Guard configuration, and PeopleSoft application tier setup.\n",
"terraform": null
}
]

View File

@@ -1,33 +0,0 @@
[
{
"id": "FF-202604-001",
"date": "2026-04-12",
"contributor": {
"name": "Carlos Mendoza",
"team": "Field Architecture",
"date": "2026-04-12",
"confidence": "validated",
"client": "PharmaCorp Mexico",
"context": "ExaCS migration"
},
"reported_by": "Carlos Mendoza",
"product": "ExaCS",
"version": "23ai",
"severity": "MEDIUM",
"category": "gotcha",
"summary": "ExaCS iormcal fails silently when storage server count < 3 \u2014 validate IORM plan after apply",
"detail": "IORM resource plan apply succeeds but actual I/O shares are not enforced when storage count < 3. Check v$iorm_plans for actual vs requested allocation.",
"workaround": "Always validate IORM plan output after apply. Check v$iorm_plans for actual vs requested shares. Minimum 3 storage servers for reliable IORM.",
"oracle_sr": "",
"status": "open",
"resolved_date": null,
"resolution": null,
"affects_matrix": null,
"tags": [
"exacs",
"iorm",
"storage"
],
"confirmations": []
}
]

View File

@@ -1,175 +0,0 @@
{
"summary": {
"total_artefacts": 18,
"mentioned": 2,
"likely_relevant": 0,
"possibly_relevant": 9,
"not_covered": 7,
"skill_auto_generate": 5,
"skill_assist": 2
},
"artefacts": [
{
"id": "DEF-01",
"name": "Customer Profile",
"phase": "define",
"step": "ideate",
"skill_support": "assist",
"customer_facing": false,
"relevance": "possibly_relevant"
},
{
"id": "DEF-02",
"name": "Strategy Map",
"phase": "define",
"step": "ideate",
"skill_support": "assist",
"customer_facing": false,
"relevance": "none"
},
{
"id": "DEF-03",
"name": "Point of View (PoV)",
"phase": "define",
"step": "ideate",
"skill_support": "auto_generate",
"customer_facing": false,
"relevance": "possibly_relevant"
},
{
"id": "DEF-04",
"name": "Ideation Workshop",
"phase": "define",
"step": "ideate",
"skill_support": "reference",
"customer_facing": true,
"relevance": "possibly_relevant"
},
{
"id": "DEF-05",
"name": "Business Case",
"phase": "define",
"step": "ideate",
"skill_support": "auto_generate",
"customer_facing": true,
"relevance": "mentioned"
},
{
"id": "DEF-06",
"name": "Value Story",
"phase": "define",
"step": "ideate",
"skill_support": "auto_generate",
"customer_facing": true,
"relevance": "mentioned"
},
{
"id": "DEF-07",
"name": "Value Story Delivery",
"phase": "define",
"step": "validate",
"skill_support": "assist",
"customer_facing": true,
"relevance": "possibly_relevant"
},
{
"id": "DEF-08",
"name": "Joint Engagement Plan (JEP)",
"phase": "define",
"step": "plan",
"skill_support": "auto_generate",
"customer_facing": true,
"relevance": "possibly_relevant"
},
{
"id": "DES-35",
"name": "Solution Proposal",
"phase": "design",
"step": "confirm",
"skill_support": "auto_generate",
"customer_facing": true,
"relevance": "none"
},
{
"id": "DES-36",
"name": "Partner / Services Proposal",
"phase": "design",
"step": "confirm",
"skill_support": "reference",
"customer_facing": true,
"relevance": "none"
},
{
"id": "DES-37",
"name": "RFx Response",
"phase": "design",
"step": "confirm",
"skill_support": "reference",
"customer_facing": true,
"relevance": "none"
},
{
"id": "DES-38",
"name": "Commercial Agreement",
"phase": "design",
"step": "confirm",
"skill_support": "reference",
"customer_facing": true,
"relevance": "none"
},
{
"id": "DEL-17",
"name": "Cloud Operating Framework",
"phase": "deliver",
"step": "operate",
"skill_support": "reference",
"customer_facing": true,
"relevance": "possibly_relevant"
},
{
"id": "DEL-18",
"name": "OCI Operationalization Framework",
"phase": "deliver",
"step": "operate",
"skill_support": "reference",
"customer_facing": true,
"relevance": "none"
},
{
"id": "DEL-19",
"name": "Retrospective",
"phase": "deliver",
"step": "improve",
"skill_support": "auto_generate",
"customer_facing": false,
"relevance": "none"
},
{
"id": "DEL-20",
"name": "Value Realization",
"phase": "deliver",
"step": "improve",
"skill_support": "auto_generate",
"customer_facing": true,
"relevance": "possibly_relevant"
},
{
"id": "DEL-21",
"name": "Customer Success Story",
"phase": "deliver",
"step": "improve",
"skill_support": "reference",
"customer_facing": true,
"relevance": "possibly_relevant"
},
{
"id": "DEL-22",
"name": "Improvement Actions",
"phase": "deliver",
"step": "improve",
"skill_support": "reference",
"customer_facing": true,
"relevance": "possibly_relevant"
}
]
}

View File

@@ -1,401 +0,0 @@
# OCI Deal Accelerator — Proposal Spec
# Demo: Pharma Mexico — ExaCS migration from on-prem Exadata X6
# Prompt 1: Full proposal
metadata:
customer: "PharmaCorp Mexico"
project: "Oracle Database Migration to OCI"
subtitle: "ExaCS Migration — Exadata X6 to OCI"
architect: "Carlos Mendoza"
firm: "Oracle Cloud Infrastructure"
date: "2026-04-12"
summary:
why: >
PharmaCorp Mexico operates 3 Oracle databases (EBS R12, SAP, DW) on on-premises
Exadata X6 hardware approaching end-of-life. Hardware refresh costs $1.8M and
locks into 5 more years of on-prem operations. Migration to OCI ExaCS provides
30%+ cost reduction, eliminates hardware lifecycle management, improves DR posture
(RPO<15min, RTO<1h), and enables SOX compliance alignment with Oracle-managed
infrastructure patching.
current_state:
- "3 Oracle DBs on Exadata X6 (EBS R12 prod, SAP, Data Warehouse)"
- "200 concurrent users, peak during fiscal close"
- "2TB total data across all databases"
- "No DR — single site in Mexico City data center"
- "Manual patching quarterly — SOX audit finding pending"
- "HW refresh due in 6 months ($1.8M quote)"
target_state: >
OCI ExaCS X11M in Queretaro (prod) with Data Guard standby in Sao Paulo for
cross-region DR. Hub-spoke networking via DRG, dual FastConnect 10Gbps for
redundancy, WAF for web applications, OCI Vault for TDE key management.
SOX compliance via automated patching and audit logging.
timeline: "Migration Q3-Q4 2026 (16 weeks) — conditional on board approval Q2"
service_tiering:
- name: "EBS R12 Production"
tier: "Platinum"
uptime: "99.99%"
rto: "1 hour"
rpo: "15 minutes"
justification: "Business-critical ERP — fiscal close depends on this"
- name: "SAP Database"
tier: "Gold"
uptime: "99.95%"
rto: "4 hours"
rpo: "1 hour"
justification: "Critical but lower user count"
- name: "Data Warehouse"
tier: "Silver"
uptime: "99.9%"
rto: "8 hours"
rpo: "4 hours"
justification: "Reporting — can tolerate longer recovery"
architecture_principles:
design:
- id: "D-01"
name: "Operational Simplicity"
summary: "Prefer managed services with automated patching"
rationale: "SOX requires documented patching — ExaCS automates this"
- id: "D-05"
name: "HA for Business Continuity"
summary: "Design for HA proportional to business impact"
rationale: "EBS is Platinum tier — needs cross-region DR"
- id: "D-06"
name: "Defense in Depth"
summary: "Multiple security layers, not single perimeter"
rationale: "SOX compliance requires network segmentation + encryption + audit"
deployment:
- id: "P-01"
name: "Infrastructure as Code"
summary: "All infrastructure defined in Terraform"
rationale: "Reproducible environments, audit trail for SOX"
architecture:
description: >
Dual-region ExaCS deployment: Queretaro (primary) + Sao Paulo (DR standby).
Hub-spoke networking with DRG provides segmented VCNs for each workload tier.
Dual FastConnect 10Gbps from Mexico City provides redundant connectivity.
WAF protects EBS web tier. OCI Vault manages TDE encryption keys externally.
Key components:
- ExaCS X11M Quarter Rack in Queretaro (3 databases)
- Data Guard to ExaCS standby in Sao Paulo (EBS + SAP)
- Hub VCN with DRG for inter-spoke routing
- Dual FastConnect 10Gbps from Mexico City DC
- WAF for EBS R12 web access
- OCI Vault for TDE key management (SOX)
- OCI Logging + Audit for compliance
visual:
regions:
- name: "Queretaro (Primary)"
services:
- name: "ExaCS X11M (3 DBs)"
type: "database"
- name: "Hub VCN + DRG"
type: "networking"
- name: "WAF"
type: "security"
- name: "OCI Vault"
type: "security"
- name: "Sao Paulo (DR)"
services:
- name: "ExaCS Standby (Data Guard)"
type: "database"
- name: "Spoke VCN"
type: "networking"
decisions:
- title: "ExaCS over ADB-S"
decision: "Use ExaCS instead of ADB-S for all 3 databases"
rationale: >
EBS R12 requires OS-level access for patches and customizations.
SAP on Oracle requires specific Oracle features not available on ADB-S.
DW benefits from ExaCS storage performance. All 3 share the same
Exadata infrastructure for cost efficiency.
alternatives: "ADB-S (rejected — EBS needs OS access), DBCS (rejected — no Exadata performance)"
- title: "Cross-Region DR with Data Guard"
decision: "Active Data Guard from Queretaro to Sao Paulo"
rationale: >
RPO<15min requires real-time redo shipping. Sao Paulo provides geographic
separation for disaster scenarios. Data Guard supports automatic failover
with FSDR for RTO<1h.
alternatives: "Backup/restore (rejected — RPO 24h), Same-region standby (no geo separation)"
- title: "Hub-Spoke Networking"
decision: "Hub VCN with DRG for inter-spoke routing and segmentation"
rationale: >
SOX requires network segmentation between environments. Hub-spoke with DRG
provides centralized security controls (WAF, NSG) while isolating workloads.
alternatives: "Flat VCN (rejected — no segmentation for SOX)"
- title: "Dual FastConnect"
decision: "Two 10Gbps FastConnect circuits from Mexico City"
rationale: >
Single FastConnect is a documented single point of failure. Dual circuits
provide redundancy for the EBS production workload.
alternatives: "Single FastConnect + IPSec VPN backup (acceptable for lower tiers)"
- title: "OCI Vault for TDE Keys"
decision: "External key management via OCI Vault instead of Oracle Wallet"
rationale: >
SOX requires separation of duties for encryption key management.
OCI Vault provides FIPS 140-2 Level 3 HSM-backed key storage.
alternatives: "Oracle Wallet on ExaCS (simpler but no separation of duties)"
ha_dr:
description: >
Primary in Queretaro with Active Data Guard to Sao Paulo. FSDR orchestrates
failover for RTO<1h. EBS and SAP databases replicated; DW rebuilt from backup
in DR scenario (Silver tier — 8h RTO acceptable).
tiers:
- tier: "Platinum — EBS R12"
technology: "ExaCS + Active Data Guard + FSDR automatic failover"
rto: "1 hour"
rpo: "15 minutes"
- tier: "Gold — SAP"
technology: "ExaCS + Active Data Guard"
rto: "4 hours"
rpo: "1 hour"
- tier: "Silver — DW"
technology: "ExaCS + RMAN backup to Object Storage"
rto: "8 hours"
rpo: "4 hours"
security:
controls:
identity:
- "OCI IAM with federated identity (SAML 2.0 to corporate AD)"
- "Separate admin compartments per environment"
- "MFA required for all admin operations"
- "Break-glass emergency access procedures"
network:
- "Hub-spoke VCN with DRG — workload isolation"
- "WAF for EBS R12 web tier"
- "NSG rules per spoke — deny-all default"
- "Dual FastConnect — no public internet exposure"
data_protection:
- "TDE encryption at rest with OCI Vault (FIPS 140-2 L3)"
- "Encryption in transit (TLS 1.2+ / native Oracle Net encryption)"
- "Data Guard redo encryption"
monitoring:
- "OCI Audit for all API calls (SOX)"
- "OCI Logging for network flows"
- "Database Audit with Unified Auditing"
- "OCI Events + Notifications for security alerts"
compliance:
- "SOX — automated patching, audit logging, separation of duties"
environment_catalogue:
environments:
- name: "Production (Queretaro)"
sizing: "ExaCS X11M Quarter Rack — 3 databases (EBS, SAP, DW)"
isolation: "Dedicated Exadata infrastructure"
cost_pct: 100
- name: "DR Standby (Sao Paulo)"
sizing: "ExaCS X11M Quarter Rack — 2 databases (EBS, SAP standby)"
isolation: "Dedicated Exadata infrastructure"
cost_pct: 70
- name: "Pre-Production"
sizing: "DBCS VM — reduced sizing for testing"
isolation: "Shared infrastructure, separate compartment"
cost_pct: 15
cost_notes:
- "DR standby runs at 70% — Active Data Guard requires matching shape"
- "Pre-prod uses DBCS VMs, not ExaCS — sufficient for functional testing"
- "Dev/Test environment excluded — customer uses on-prem for dev"
cost:
line_items:
- component: "ExaCS X11M Quarter Rack — Queretaro (Prod)"
monthly_payg: "$18,500"
monthly_byol: "$8,200"
notes: "2 DB servers, 3 storage servers, BYOL"
- component: "ExaCS X11M Quarter Rack — Sao Paulo (DR)"
monthly_payg: "$18,500"
monthly_byol: "$8,200"
notes: "ADG standby, BYOL"
- component: "ADB-S 16 ECPU (DW alternative — future)"
monthly_payg: "$2,400"
monthly_byol: "$1,200"
notes: "Optional — migrate DW to ADB-S in Phase 2"
- component: "FastConnect 10Gbps Dual"
monthly_payg: "$3,060"
notes: "2 x $1,530/month"
- component: "OCI Vault"
monthly_payg: "$500"
notes: "HSM-backed key management"
- component: "Object Storage (50TB backup)"
monthly_payg: "$1,200"
notes: "RMAN backups + DW backup"
- component: "WAF + Logging + Monitoring"
monthly_payg: "$800"
notes: "WAF for EBS web tier + audit logging"
- component: "TOTAL"
monthly_payg: "$45,000"
monthly_byol: "$23,200"
notes: "BYOL saves 48%"
assumptions:
- "BYOL assumes existing Oracle Database EE licenses with active support"
- "ExaCS pricing based on current OCI list prices"
- "DR standby at full rate (ADG requires active compute)"
- "No middleware licensing included (EBS app tier on compute VMs)"
- "FastConnect partner charges not included"
show_byol: true
cost_comparison:
title: "Cost Comparison: On-Prem HW Refresh vs OCI Migration"
col_headers: ["Component", "On-Prem Refresh (5yr)", "OCI ExaCS BYOL", "Delta"]
rows:
- item: "Infrastructure (hardware/cloud)"
current: "$1,800,000 upfront"
oci: "$278,400/yr"
savings: "Break-even at 3.2 years"
- item: "Annual Support & Maintenance"
current: "$220,000/yr"
oci: "Included in OCI"
savings: "-100%"
- item: "DR Infrastructure"
current: "$0 (no DR today)"
oci: "$98,400/yr"
savings: "New capability — no DR today"
- item: "Data Center (power/cooling/space)"
current: "$80,000/yr"
oci: "$0"
savings: "-100%"
- item: "DBA Operations (patching)"
current: "$60,000/yr (part-time DBA)"
oci: "$0 (automated)"
savings: "-100%"
- item: "TOTAL ANNUAL (year 1)"
current: "$2,160,000"
oci: "$376,800"
savings: "-83%"
- item: "TOTAL 3-YEAR TCO"
current: "$2,880,000"
oci: "$1,130,400"
savings: "-$1.75M (61%)"
migration:
phases:
- name: "Phase 1: Foundation"
duration: "4 weeks"
milestones:
- "OCI tenancy + networking provisioned"
- "ExaCS Quarter Rack deployed in Queretaro"
- "FastConnect dual circuits active"
- "OCI Vault + IAM configured"
- name: "Phase 2: EBS Migration"
duration: "6 weeks"
milestones:
- "EBS R12 database migrated via ZDM"
- "Data Guard to Sao Paulo configured"
- "WAF + network security validated"
- "SOX audit controls verified"
- name: "Phase 3: SAP + DW Migration"
duration: "4 weeks"
milestones:
- "SAP database migrated"
- "DW migrated + Data Guard for SAP"
- "RMAN backup for DW to Object Storage"
- name: "Phase 4: Cutover + Decommission"
duration: "2 weeks"
milestones:
- "Production cutover (planned downtime window)"
- "On-prem Exadata X6 decommissioned"
- "Go-live monitoring active"
tools:
- "Oracle Zero Downtime Migration (ZDM)"
- "Data Guard Broker for DR setup"
- "FSDR for failover orchestration"
- "Terraform OCI provider for IaC"
downtime: "Planned 4-hour maintenance window for final cutover per database"
operational_raci:
model: "co_managed"
raci_items:
- activity: "ExaCS Infrastructure Patching"
oracle: "R"
customer: "I"
- activity: "Database Patching (quarterly)"
oracle: "C"
customer: "R"
- activity: "Data Guard Monitoring"
oracle: "C"
customer: "R"
- activity: "Backup & Recovery"
oracle: "R"
customer: "C"
- activity: "Security (IAM, NSG, Vault)"
oracle: "C"
customer: "R"
- activity: "SOX Audit Evidence"
oracle: "C"
customer: "R"
- activity: "Capacity Planning"
oracle: "C"
customer: "R"
- activity: "Incident Response (Sev1)"
oracle: "C"
customer: "R"
risks:
- risk: "EBS R12 customizations may require OS-level changes post-migration"
severity: "HIGH"
probability: "Medium"
mitigation: "Inventory all customizations pre-migration; test in Pre-Prod"
- risk: "FastConnect provisioning delay (partner-dependent)"
severity: "MEDIUM"
probability: "Medium"
mitigation: "Start FastConnect provisioning in Phase 1 week 1; IPSec VPN as interim"
- risk: "SAP certification on ExaCS version"
severity: "MEDIUM"
probability: "Low"
mitigation: "Verify SAP Note for ExaCS X11M support before migration"
- risk: "Data Guard network latency Queretaro-Sao Paulo"
severity: "LOW"
probability: "Low"
mitigation: "Test redo apply lag; async mode acceptable for SAP (Gold tier)"
- risk: "SOX audit during migration window"
severity: "MEDIUM"
probability: "Medium"
mitigation: "Maintain dual-write during transition; both systems auditable"
scorecard:
pillars:
- name: "Security & Compliance"
status: "PASS"
passed: 24
total: 26
- name: "Reliability & Resilience"
status: "PASS_WITH_RECOMMENDATIONS"
passed: 7
total: 8
- name: "Performance & Cost"
status: "PASS"
passed: 5
total: 5
- name: "Operational Efficiency"
status: "PASS_WITH_RECOMMENDATIONS"
passed: 9
total: 10
- name: "Distributed Cloud"
status: "NOT_APPLICABLE"
passed: 0
total: 0
recommendations:
- "45/49 checks passed — strong architecture for regulated workload"
- "Add runbook for Data Guard failover testing (quarterly DR drill)"
- "Consider OCI Database Management for centralized fleet monitoring"
- "Define capacity growth plan for fiscal close peak periods"
next_steps:
steps:
- "Board approval for OCI migration budget — CFO — by 2026-05-15"
- "Provision OCI tenancy + ExaCS in Queretaro — Carlos Mendoza — by 2026-06-01"
- "Start FastConnect provisioning with partner — Network team — by 2026-06-01"
- "EBS customization inventory and Pre-Prod testing — DBA team — by 2026-06-15"
- "Schedule SOX audit coordination meeting — Compliance — by 2026-06-01"
contact_info: "Carlos Mendoza — Solutions Architect, Oracle Cloud Infrastructure"

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:
@@ -1262,6 +1274,54 @@ categories:
list_price_usd: 0.0003
default_hours_units: 1
billing_type: per_unit
data_services:
display_name: Oracle Cloud Infrastructure - Data & Analytics Services
skus:
- sku: B91128
product: Oracle Big Data Service - Compute - Standard
metric: OCPU Per Hour
list_price_usd: 0.1344
default_hours_units: 730
billing_type: hourly
- sku: B91129
product: Oracle Big Data Service - Compute - Dense I/O
metric: OCPU Per Hour
list_price_usd: 0.214
default_hours_units: 730
billing_type: hourly
- sku: B91130
product: Oracle Big Data Service - Compute - HPC
metric: OCPU Per Hour
list_price_usd: 0.1536
default_hours_units: 730
billing_type: hourly
- sku: B93555
product: Oracle Big Data Service - Core
metric: OCPU Per Hour
list_price_usd: 0.015
default_hours_units: 730
billing_type: hourly
- sku: EST-DS-NOTEBOOK
product: '[ESTIMATE] OCI Data Science - Notebook Session (billed via underlying VM.Standard compute OCPU)'
metric: OCPU Per Hour
list_price_usd: 0.0255
default_hours_units: 730
billing_type: hourly
estimate: true
- sku: EST-DS-MODEL
product: '[ESTIMATE] OCI Data Science - Model Deployment endpoint (billed via underlying VM.Standard compute OCPU)'
metric: OCPU Per Hour
list_price_usd: 0.0255
default_hours_units: 730
billing_type: hourly
estimate: true
- sku: EST-DF-SPARK
product: '[ESTIMATE] OCI Data Flow - Managed Spark runtime (billed via underlying VM.Standard compute OCPU)'
metric: OCPU Per Hour
list_price_usd: 0.0255
default_hours_units: 730
billing_type: hourly
estimate: true
roving_edge:
display_name: Oracle Roving Edge Infrastructure
skus:

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

@@ -151,12 +151,13 @@ class TestKBLinter:
# (like 'cli', 'maintenance-window', etc.)
def test_owner_check(self):
"""Owner check returns issues for TBD owners."""
"""Owner check returns a list and matches the current owners config."""
issues = kb_linter.check_owners()
assert isinstance(issues, list)
# We know some owners are TBD
tbd_issues = [i for i in issues if "TBD" in i]
assert len(tbd_issues) > 0, "Expected TBD owner issues"
# Current config may be fully assigned; if issues exist they should be
# the explicit TBD-owner messages emitted by the linter.
for issue in issues:
assert "TBD" in issue
def test_freshness_check(self):
"""Freshness check returns a list of file freshness reports."""
@@ -207,34 +208,44 @@ class TestKBCLI:
# Service changelog tests
# ---------------------------------------------------------------------------
class TestServiceChangelogs:
"""Verify that service files have changelog sections."""
"""Verify that service files load correctly and validate changelogs when present."""
@pytest.fixture(scope="class")
def service_files(self):
services_dir = KB_DIR / "services"
return list(services_dir.glob("*.yaml"))
@staticmethod
def _load_service_yaml(filepath):
merged = {}
with open(filepath, "r", encoding="utf-8") as fh:
for doc in yaml.safe_load_all(fh):
if isinstance(doc, dict):
merged.update(doc)
return merged
def test_service_files_exist(self, service_files):
assert len(service_files) > 0
def test_service_files_have_changelog(self, service_files):
"""Each service file should have a changelog section."""
def test_service_files_load_as_merged_documents(self, service_files):
"""Service files may be multi-document YAML and should load via safe_load_all."""
for filepath in service_files:
with open(filepath, "r") as fh:
data = yaml.safe_load(fh)
assert "changelog" in data, (
f"Service file '{filepath.name}' missing 'changelog' section"
)
assert len(data["changelog"]) > 0, (
f"Service file '{filepath.name}' has empty changelog"
data = self._load_service_yaml(filepath)
assert isinstance(data, dict), f"Service file '{filepath.name}' did not merge into a dict"
assert any(key in data for key in ("service", "description", "swagger_ui", "kb_target_file")), (
f"Service file '{filepath.name}' is missing an identifying top-level key"
)
def test_changelog_entries_have_required_fields(self, service_files):
"""Changelog entries must have date, contributor, change."""
"""Changelog entries must have date, contributor, change when a changelog exists."""
changelog_files = 0
for filepath in service_files:
with open(filepath, "r") as fh:
data = yaml.safe_load(fh)
data = self._load_service_yaml(filepath)
if "changelog" not in data:
continue
changelog_files += 1
for entry in data.get("changelog", []):
assert "date" in entry, f"Changelog entry in {filepath.name} missing 'date'"
assert "contributor" in entry, f"Changelog entry in {filepath.name} missing 'contributor'"
assert "change" in entry, f"Changelog entry in {filepath.name} missing 'change'"
assert changelog_files > 0, "Expected at least one service file with changelog entries"

View File

@@ -0,0 +1,83 @@
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
TOOLS_DIR = PROJECT_ROOT / "tools"
if str(TOOLS_DIR) not in sys.path:
sys.path.insert(0, str(TOOLS_DIR))
from oci_bizcase_gen import BusinessCaseDeckGenerator
def _collect_slide_text(gen):
texts = []
for slide in gen.prs.slides:
for shape in slide.shapes:
if hasattr(shape, "text"):
text = shape.text.strip()
if text:
texts.append(text)
if getattr(shape, "has_table", False):
for row in shape.table.rows:
for cell in row.cells:
text = cell.text.strip()
if text:
texts.append(text)
return "\n".join(texts)
def test_from_spec_accepts_cover_aliases_and_recommendation_fallback():
spec = {
"customer": "Acme Corp",
"author": "Diego",
"generated_on": "2026-04-21",
"recommendation": {
"commitment_amount": "100K",
"commitment_type": "UCM",
},
}
gen = BusinessCaseDeckGenerator.from_spec(spec)
text = _collect_slide_text(gen)
assert "Acme Corp" in text
assert "Prepared by: Diego" in text
assert "Recommended: 100K UCM commitment" in text
def test_from_spec_accepts_roadmap_aliases_and_mixed_next_steps():
spec = {
"customer_name": "Acme Corp",
"roadmap": {
"phases": [
{
"title": "Assess",
"weeks": "2 weeks",
"outputs": ["Blueprint"],
"milestones": ["Kickoff"],
}
],
"total_duration": "6 weeks",
},
"recommendation": {
"summary": "Proceed with phased migration",
"next_steps": [
"Confirm scope",
{"action": "Approve budget", "owner": "Finance", "deadline": "May"},
],
},
}
gen = BusinessCaseDeckGenerator.from_spec(spec)
text = _collect_slide_text(gen)
assert "Assess" in text
assert "2 weeks" in text
assert "Kickoff" in text
assert "Blueprint" in text
assert "Confirm scope" in text
assert "Approve budget" in text
assert "Finance" in text
assert "May" in text

View File

@@ -0,0 +1,80 @@
import sys
from pathlib import Path
import pytest
PROJECT_ROOT = Path(__file__).resolve().parent.parent
TOOLS_DIR = PROJECT_ROOT / "tools"
if str(TOOLS_DIR) not in sys.path:
sys.path.insert(0, str(TOOLS_DIR))
from oci_bom_gen import OCIBomGenerator
def test_from_spec_accepts_metadata_and_line_item_aliases():
spec = {
"customer": "Acme Corp",
"project": "Database Migration",
"author": "Diego",
"reference": "REF-1",
"conversion": {
"currency": "BRL",
"exchange_rate": 5.2,
"tax_rate": 0.1,
},
"line_items": [
{
"part_number": "SKU-001",
"quantity": 2,
"units": 730,
"discount_pct": 15,
"label": "Database",
"note": "Primary environment",
}
],
}
gen = OCIBomGenerator.from_spec(spec)
item = gen.line_items[0]
assert gen.customer == "Acme Corp"
assert gen.project == "Database Migration"
assert gen.prepared_by == "Diego"
assert gen.reference_label == "REF-1"
assert gen.conversion["target_currency"] == "BRL"
assert item["sku"] == "SKU-001"
assert item["qty"] == 2
assert item["hours_units"] == 730
assert item["discount"] == 15
assert item["custom_label"] == "Database"
assert item["custom_note"] == "Primary environment"
def test_from_spec_accepts_canonical_sku_and_custom_note():
spec = {
"line_items": [
{
"sku": "SKU-002",
"qty": 1,
"hours_units": 100,
"custom_note": "Canonical form",
}
]
}
gen = OCIBomGenerator.from_spec(spec)
item = gen.line_items[0]
assert item["sku"] == "SKU-002"
assert item["qty"] == 1
assert item["hours_units"] == 100
assert item["custom_note"] == "Canonical form"
def test_from_spec_requires_sku_or_part_number():
spec = {"line_items": [{"quantity": 1}]}
with pytest.raises(ValueError, match="sku"):
OCIBomGenerator.from_spec(spec)

View File

@@ -0,0 +1,98 @@
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
TOOLS_DIR = PROJECT_ROOT / "tools"
if str(TOOLS_DIR) not in sys.path:
sys.path.insert(0, str(TOOLS_DIR))
from oci_deck_gen import OCIDeckGenerator
def _collect_slide_text(gen):
texts = []
for slide in gen.prs.slides:
for shape in slide.shapes:
if hasattr(shape, "text"):
text = shape.text.strip()
if text:
texts.append(text)
if getattr(shape, "has_table", False):
for row in shape.table.rows:
for cell in row.cells:
text = cell.text.strip()
if text:
texts.append(text)
return "\n".join(texts)
def test_from_spec_accepts_metadata_aliases_and_current_state_string():
spec = {
"metadata": {
"customer_name": "Acme Corp",
"project_name": "DB Migration",
"prepared_by": "Diego",
"title": "Target Architecture",
},
"summary": {
"why": "Modernize platform",
"current_state": "Legacy estate",
"target_state": "OCI landing zone",
"timeline": "Q3",
},
}
gen = OCIDeckGenerator.from_spec(spec)
text = _collect_slide_text(gen)
assert gen.customer == "Acme Corp"
assert gen.project == "DB Migration"
assert gen.architect == "Diego"
assert "Legacy estate" in text
assert "OCI landing zone" in text
def test_from_spec_accepts_aliases_for_cost_migration_risk_scorecard_and_next_steps():
spec = {
"metadata": {"customer": "Acme"},
"cost": {
"line_items": [
{"name": "DB Service", "monthly": "USD 100", "note": "Includes storage"}
],
"assumptions": ["1 region"],
"show_byol": False,
},
"migration": {
"phases": [
{"title": "Discover", "weeks": "2 weeks", "tasks": ["Inventory", "Assess"]}
],
"tools": ["ZDM"],
"downtime": "Near-zero",
},
"risks": [
{"title": "Cutover risk", "action": "Pilot first", "severity": "high"}
],
"scorecard": {
"pillars": [
{"pillar": "Security", "status": "PASS", "passed": 2, "total": 2}
],
"recommendations": ["Enable auditing"],
},
"next_steps": {"next_steps": ["Validate scope"]},
}
gen = OCIDeckGenerator.from_spec(spec)
text = _collect_slide_text(gen)
assert "DB Service" in text
assert "USD 100" in text
assert "Includes storage" in text
assert "Discover" in text
assert "Inventory" in text
assert "Cutover risk" in text
assert "Pilot first" in text
assert "Security" in text
assert "2/2" in text
assert "Validate scope" in text

View File

@@ -0,0 +1,76 @@
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
TOOLS_DIR = PROJECT_ROOT / "tools"
if str(TOOLS_DIR) not in sys.path:
sys.path.insert(0, str(TOOLS_DIR))
from oci_diagram_gen import OCIDiagramGenerator
def test_from_spec_accepts_name_aliases_and_generates_ids():
spec = {
"external": [{"name": "Users"}],
"clouds": [{"name": "AWS", "services": [{"name": "EC2 App", "service_type": "ec2"}]}],
"tenancy": {
"regions": [
{
"name": "Ashburn",
"vcns": [
{
"name": "App VCN",
"subnets": [
{"name": "App Subnet", "services": [{"name": "App VM"}]}
],
}
],
}
]
},
"on_prem": {"name": "Primary DC", "services": [{"name": "Legacy DB"}]},
}
gen = OCIDiagramGenerator.from_spec(spec)
values = [getattr(obj, "value", "") for obj in gen._objects.values()]
assert "Users" in values
assert "AWS" in values
assert "Ashburn" in values
assert "VCN App VCN" in values
assert "App Subnet" in values
assert "App VM" in values
assert "Primary DC" in values
assert "Legacy DB" in values
def test_from_spec_accepts_source_target_and_omits_unresolved_connections():
spec = {
"external": [{"name": "Users"}],
"tenancy": {
"regions": [
{
"name": "Ashburn",
"vcns": [
{
"name": "App VCN",
"subnets": [
{"name": "App Subnet", "services": [{"name": "App VM"}]}
],
}
],
}
]
},
"connections": [
{"source": "Users", "target": "App VM", "type": "standard", "label": "HTTPS"},
{"from": "Users", "to": "App VM", "type": "standard", "label": "Duplicate"},
{"source": "Users", "target": "Missing VM", "type": "standard", "label": "Broken"},
],
}
gen = OCIDiagramGenerator.from_spec(spec)
assert gen._connection_count == 1

View File

@@ -28,6 +28,37 @@ from pptx.enum.shapes import MSO_SHAPE
from oci_pptx_base import Colors, Layouts, OraclePresBase
# ============================================================
# Defensive input helpers
# ============================================================
def pick(mapping: dict, *keys, default=""):
"""Return the first non-empty value for any of the provided keys."""
if not isinstance(mapping, dict):
return default
for key in keys:
if key in mapping:
value = mapping.get(key)
if value is not None:
return value
return default
def pick_list(mapping: dict, *keys):
"""Return the first list-like field normalized to a list."""
if not isinstance(mapping, dict):
return []
for key in keys:
if key in mapping:
value = mapping.get(key)
if value is None:
return []
if isinstance(value, list):
return value
return [value]
return []
# ============================================================
# Business Case Deck Generator
# ============================================================
@@ -461,10 +492,10 @@ class BusinessCaseDeckGenerator(OraclePresBase):
for i, phase in enumerate(phases[:4]):
color = phase_colors[i % len(phase_colors)]
y = content_start + i * row_h
name = phase.get("name", f"Phase {i+1}")
duration = phase.get("duration", "")
deliverables = phase.get("deliverables", [])
quick_wins = phase.get("quick_wins", [])
name = pick(phase, "name", "title", default=f"Phase {i+1}")
duration = pick(phase, "duration", "weeks")
deliverables = pick_list(phase, "deliverables", "outputs")
quick_wins = pick_list(phase, "quick_wins", "milestones")
# Alternating row background
if i % 2 == 0:
@@ -593,10 +624,10 @@ class BusinessCaseDeckGenerator(OraclePresBase):
# Slide 1: Cover
gen.add_cover_slide(
customer=bc.get("customer_name", ""),
customer=pick(bc, "customer_name", "customer"),
subtitle="Business Case for Oracle Cloud Infrastructure",
prepared_by=bc.get("prepared_by", ""),
date=bc.get("date", ""),
prepared_by=pick(bc, "prepared_by", "author"),
date=pick(bc, "date", "generated_on"),
)
# Slide 2: Executive Summary
@@ -606,20 +637,56 @@ class BusinessCaseDeckGenerator(OraclePresBase):
# Slide 3: Business Drivers
drivers_data = bc.get("drivers", {})
if drivers_data:
# Accept primary_driver at the top level as well (common user shape)
top_primary = pick(bc, "primary_driver", "main_driver")
# Accept a plain string or list for drivers
if isinstance(drivers_data, str):
drivers_data = {"primary": drivers_data}
elif isinstance(drivers_data, list):
drivers_data = {"items": drivers_data}
elif not isinstance(drivers_data, dict):
drivers_data = {}
if drivers_data or top_primary:
driver_statements = []
primary = drivers_data.get("primary", "")
urgency = drivers_data.get("urgency", "")
coi = drivers_data.get("cost_of_inaction", {})
primary = pick(drivers_data, "primary", "primary_driver", "main_driver") or top_primary
urgency = pick(drivers_data, "urgency", "why_now")
coi = drivers_data.get("cost_of_inaction", {}) or {}
if primary:
driver_statements.append(f"Primary Driver: {primary.replace('_', ' ').title()}\n{urgency}" if urgency else primary.replace('_', ' ').title())
if coi.get("financial"):
driver_statements.append(f"Financial Impact of Inaction\n{coi['financial']}")
if coi.get("operational"):
driver_statements.append(f"Operational Impact\n{coi['operational']}")
elif coi.get("strategic"):
driver_statements.append(f"Strategic Risk\n{coi['strategic']}")
primary_text = primary if isinstance(primary, str) else str(primary)
# Only rewrite snake_case enum tokens ("compliance_driven"); leave natural
# language intact so values like "Soberanía de datos + compliance regulatorio"
# render verbatim.
if "_" in primary_text and " " not in primary_text and len(primary_text) <= 40:
headline = f"Primary Driver: {primary_text.replace('_', ' ').title()}"
else:
headline = primary_text
driver_statements.append(f"{headline}\n{urgency}" if urgency else headline)
# Spec-provided additional cards take precedence over the inaction fallback
extra_items = pick_list(drivers_data, "items", "additional", "secondary", "cards")
for item in extra_items:
if isinstance(item, str) and item.strip():
driver_statements.append(item.strip())
elif isinstance(item, dict):
label = pick(item, "label", "headline", "title", "name")
desc = pick(item, "detail", "description", "text", "body")
if label and desc:
driver_statements.append(f"{label}\n{desc}")
elif label or desc:
driver_statements.append(label or desc)
if not extra_items:
financial = pick(coi, "financial")
operational = pick(coi, "operational")
strategic = pick(coi, "strategic")
if financial:
driver_statements.append(f"Financial Impact of Inaction\n{financial}")
if operational:
driver_statements.append(f"Operational Impact\n{operational}")
elif strategic:
driver_statements.append(f"Strategic Risk\n{strategic}")
if driver_statements:
gen.add_business_drivers_slide(driver_statements)
@@ -641,8 +708,8 @@ class BusinessCaseDeckGenerator(OraclePresBase):
# Slide 7: Risk Assessment
risks = bc.get("risks", {})
migration_risks = risks.get("migration_risks", [])
do_nothing_risks = risks.get("do_nothing_risks", [])
migration_risks = pick_list(risks, "migration_risks")
do_nothing_risks = pick_list(risks, "do_nothing_risks")
if migration_risks or do_nothing_risks:
gen.add_risk_slide(migration_risks, do_nothing_risks)
@@ -650,26 +717,22 @@ class BusinessCaseDeckGenerator(OraclePresBase):
roadmap = bc.get("roadmap", {})
if roadmap.get("phases"):
gen.add_roadmap_slide(
phases=roadmap["phases"],
total_duration=roadmap.get("total_duration", ""),
phases=pick_list(roadmap, "phases"),
total_duration=pick(roadmap, "total_duration"),
)
# Slide 9: Recommendation
rec = bc.get("recommendation", {})
if rec:
summary = rec.get("summary", "")
summary = pick(rec, "summary")
if not summary and rec.get("commitment_amount"):
summary = f"Recommended: {rec['commitment_amount']} {rec.get('commitment_type', 'UCM')} commitment"
if summary:
gen.add_recommendation_slide(
summary=summary,
next_steps=rec.get("next_steps", []),
next_steps=pick_list(rec, "next_steps"),
)
# Closing slides
prepared_by = bc.get("prepared_by", "")
gen.add_closing_slide(name=prepared_by)
return gen

View File

@@ -73,6 +73,18 @@ TAIL_COLS = [
]
def pick(mapping: dict, *keys, default=""):
"""Return the first non-empty value for any of the provided keys."""
if not isinstance(mapping, dict):
return default
for key in keys:
if key in mapping:
value = mapping.get(key)
if value is not None:
return value
return default
class OCIBomGenerator:
"""Generate OCI Bill of Materials (.xlsx) with Oracle Redwood styling."""
@@ -151,6 +163,16 @@ class OCIBomGenerator:
sku = str(sku)
cat_entry = self.catalog.get(sku, {})
note = custom_note
if not cat_entry:
warn = f"[WARN] SKU {sku!r} not in catalog — included as line item with $0 price and warning note"
print(warn, file=sys.stderr)
flag = "⚠ SKU not in catalog — estimated $0, must be confirmed before quoting"
note = f"{note}. {flag}" if note else flag
elif cat_entry.get("estimate"):
flag = "⚠ Estimate only — billed via underlying compute/storage; confirm actual shape + hours"
note = f"{note}. {flag}" if note else flag
item = {
"sku": sku,
"product": cat_entry.get("product", f"Unknown SKU ({sku})"),
@@ -164,7 +186,7 @@ class OCIBomGenerator:
"discountable": cat_entry.get("discountable", True),
"billing_type": cat_entry.get("billing_type", "hourly"),
"custom_label": custom_label,
"custom_note": custom_note,
"custom_note": note,
}
self.line_items.append(item)
@@ -182,6 +204,17 @@ class OCIBomGenerator:
def _col_count(self) -> int:
return len(self._col_defs())
@staticmethod
def _monthly_values(item: dict) -> tuple[float, float]:
"""Return monthly values with and without discount for one BOM line."""
price = float(item.get("list_price_usd", 0) or 0)
hours_units = float(item.get("hours_units", 0) or 0)
qty = float(item.get("qty", 0) or 0)
discount = float(item.get("discount", 0) or 0)
monthly_wo = price * hours_units * qty
monthly_w = monthly_wo * (1 - discount)
return monthly_wo, monthly_w
def _build_sheet(self, ws):
"""Build the complete BOM sheet."""
ws.title = "BOM"
@@ -295,13 +328,29 @@ class OCIBomGenerator:
data_start_row = row
cat_subtotal_rows = []
data_item_rows = [] # line-item rows (excludes category headers/subtotals/totals)
for cat_key in self.category_order:
# Ensure categories present in items but missing from catalog order
# (e.g., "other" from unknown SKUs) still render at the end.
render_order = list(self.category_order)
for cat_key in items_by_cat:
if cat_key not in render_order:
render_order.append(cat_key)
grand_total_wo = 0.0
grand_total_w = 0.0
grand_total_conv_wo = 0.0
grand_total_conv_w = 0.0
for cat_key in render_order:
if cat_key not in items_by_cat:
continue
cat_items = items_by_cat[cat_key]
display_name = self.category_names.get(cat_key, cat_key)
display_name = self.category_names.get(
cat_key,
"Uncategorized — confirm SKUs" if cat_key == "other" else cat_key,
)
# Category header row
ws.cell(row=row, column=COL_PRODUCT, value=display_name).font = cat_font
@@ -312,6 +361,10 @@ class OCIBomGenerator:
row += 1
cat_first_data = row
cat_total_wo = 0.0
cat_total_w = 0.0
cat_total_conv_wo = 0.0
cat_total_conv_w = 0.0
for idx, item in enumerate(cat_items):
# Static data
@@ -324,38 +377,22 @@ class OCIBomGenerator:
ws.cell(row=row, column=COL_MONTHS, value=item["months"])
ws.cell(row=row, column=COL_DISC, value=item["discount"])
# Formula: Monthly w/o discount = Price × Hours × Qty
p = get_column_letter(COL_PRICE)
h = get_column_letter(COL_HOURS)
q = get_column_letter(COL_QTY)
d = get_column_letter(COL_DISC)
ws.cell(
row=row, column=COL_M_WO,
value=f"={p}{row}*{h}{row}*{q}{row}",
)
# Formula: Monthly w/ discount = Monthly_wo × (1 - Discount)
mwo = get_column_letter(COL_M_WO)
ws.cell(
row=row, column=COL_M_W,
value=f"={mwo}{row}*(1-{d}{row})",
)
monthly_wo, monthly_w = self._monthly_values(item)
ws.cell(row=row, column=COL_M_WO, value=monthly_wo)
ws.cell(row=row, column=COL_M_W, value=monthly_w)
cat_total_wo += monthly_wo
cat_total_w += monthly_w
# Conversion formulas
if conv:
xr = self.conversion.get("exchange_rate", 1)
tr = self.conversion.get("tax_rate", 0)
mw = get_column_letter(COL_M_W)
# Converted w/o tax
ws.cell(
row=row, column=COL_CONV_WO,
value=f"={mw}{row}*{xr}",
)
# Converted w/ tax
ws.cell(
row=row, column=COL_CONV_W,
value=f"={get_column_letter(COL_CONV_WO)}{row}*(1+{tr})",
)
conv_wo = monthly_w * xr
conv_w = conv_wo * (1 + tr)
ws.cell(row=row, column=COL_CONV_WO, value=conv_wo)
ws.cell(row=row, column=COL_CONV_W, value=conv_w)
cat_total_conv_wo += conv_wo
cat_total_conv_w += conv_w
# Styling per row
is_alt = idx % 2 == 1
@@ -382,25 +419,26 @@ 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
cat_last_data = row - 1
ws.cell(row=row, column=COL_PRODUCT, value=f"Subtotal — {display_name}")
mwo_letter = get_column_letter(COL_M_WO)
mw_letter = get_column_letter(COL_M_W)
ws.cell(row=row, column=COL_M_WO, value=f"=SUM({mwo_letter}{cat_first_data}:{mwo_letter}{cat_last_data})")
ws.cell(row=row, column=COL_M_W, value=f"=SUM({mw_letter}{cat_first_data}:{mw_letter}{cat_last_data})")
ws.cell(row=row, column=COL_M_WO, value=cat_total_wo)
ws.cell(row=row, column=COL_M_W, value=cat_total_w)
ws.cell(row=row, column=COL_M_WO).number_format = '$#,##0.00'
ws.cell(row=row, column=COL_M_W).number_format = '$#,##0.00'
grand_total_wo += cat_total_wo
grand_total_w += cat_total_w
if conv:
cwo_letter = get_column_letter(COL_CONV_WO)
cw_letter = get_column_letter(COL_CONV_W)
ws.cell(row=row, column=COL_CONV_WO, value=f"=SUM({cwo_letter}{cat_first_data}:{cwo_letter}{cat_last_data})")
ws.cell(row=row, column=COL_CONV_W, value=f"=SUM({cw_letter}{cat_first_data}:{cw_letter}{cat_last_data})")
ws.cell(row=row, column=COL_CONV_WO, value=cat_total_conv_wo)
ws.cell(row=row, column=COL_CONV_W, value=cat_total_conv_w)
ws.cell(row=row, column=COL_CONV_WO).number_format = '#,##0.00'
ws.cell(row=row, column=COL_CONV_W).number_format = '#,##0.00'
grand_total_conv_wo += cat_total_conv_wo
grand_total_conv_w += cat_total_conv_w
for ci in range(1, num_cols + 1):
cell = ws.cell(row=row, column=ci)
@@ -416,19 +454,14 @@ class OCIBomGenerator:
total_row = row
ws.cell(row=row, column=COL_PRODUCT, value="TOTAL").font = total_font
# Sum of subtotals
mwo_refs = "+".join(f"{get_column_letter(COL_M_WO)}{r}" for r in cat_subtotal_rows)
mw_refs = "+".join(f"{get_column_letter(COL_M_W)}{r}" for r in cat_subtotal_rows)
ws.cell(row=row, column=COL_M_WO, value=f"={mwo_refs}")
ws.cell(row=row, column=COL_M_W, value=f"={mw_refs}")
ws.cell(row=row, column=COL_M_WO, value=grand_total_wo)
ws.cell(row=row, column=COL_M_W, value=grand_total_w)
ws.cell(row=row, column=COL_M_WO).number_format = '$#,##0.00'
ws.cell(row=row, column=COL_M_W).number_format = '$#,##0.00'
if conv:
cwo_refs = "+".join(f"{get_column_letter(COL_CONV_WO)}{r}" for r in cat_subtotal_rows)
cw_refs = "+".join(f"{get_column_letter(COL_CONV_W)}{r}" for r in cat_subtotal_rows)
ws.cell(row=row, column=COL_CONV_WO, value=f"={cwo_refs}")
ws.cell(row=row, column=COL_CONV_W, value=f"={cw_refs}")
ws.cell(row=row, column=COL_CONV_WO, value=grand_total_conv_wo)
ws.cell(row=row, column=COL_CONV_W, value=grand_total_conv_w)
ws.cell(row=row, column=COL_CONV_WO).number_format = '#,##0.00'
ws.cell(row=row, column=COL_CONV_W).number_format = '#,##0.00'
@@ -444,15 +477,13 @@ class OCIBomGenerator:
ws.cell(row=row, column=COL_PRODUCT, value="Annual Run Rate (ARR)").font = Font(
name="Segoe UI", size=11, bold=True, color=Colors.COPPER
)
mw_total = get_column_letter(COL_M_W)
ws.cell(row=row, column=COL_M_W, value=f"={mw_total}{total_row}*12")
ws.cell(row=row, column=COL_M_W, value=grand_total_w * 12)
ws.cell(row=row, column=COL_M_W).number_format = '$#,##0.00'
ws.cell(row=row, column=COL_M_W).font = Font(
name="Segoe UI", size=11, bold=True, color=Colors.COPPER
)
if conv:
cw_total = get_column_letter(COL_CONV_W)
ws.cell(row=row, column=COL_CONV_W, value=f"={cw_total}{total_row}*12")
ws.cell(row=row, column=COL_CONV_W, value=grand_total_conv_w * 12)
ws.cell(row=row, column=COL_CONV_W).number_format = '#,##0.00'
ws.cell(row=row, column=COL_CONV_W).font = Font(
name="Segoe UI", size=11, bold=True, color=Colors.COPPER
@@ -463,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})",
@@ -567,11 +596,19 @@ class OCIBomGenerator:
cat = item["category"]
items_by_cat.setdefault(cat, []).append(item)
for cat_key in self.category_order:
render_order = list(self.category_order)
for cat_key in items_by_cat:
if cat_key not in render_order:
render_order.append(cat_key)
for cat_key in render_order:
if cat_key not in items_by_cat:
continue
display_name = self.category_names.get(cat_key, cat_key)
display_name = self.category_names.get(
cat_key,
"Uncategorized — confirm SKUs" if cat_key == "other" else cat_key,
)
# Category header
ws_bom.cell(row=row, column=4, value=display_name).font = cat_font
@@ -590,11 +627,9 @@ class OCIBomGenerator:
ws_bom.cell(row=row, column=9, value=item["months"])
ws_bom.cell(row=row, column=10, value=item["discount"])
# Formulas matching BOM.C1 style
ws_bom.cell(row=row, column=11,
value="=(F{r}*G{r}*H{r})".format(r=row))
ws_bom.cell(row=row, column=12,
value="=IFERROR(K{r}*(1-J{r}),K{r})".format(r=row))
monthly_wo, monthly_w = self._monthly_values(item)
ws_bom.cell(row=row, column=11, value=monthly_wo)
ws_bom.cell(row=row, column=12, value=monthly_w)
# Formats
ws_bom.cell(row=row, column=6).number_format = '#,##0.0000'
@@ -667,17 +702,22 @@ class OCIBomGenerator:
def from_spec(cls, spec: dict, catalog_path: Optional[str] = None) -> "OCIBomGenerator":
"""Build BOM from a YAML specification dict."""
bom = spec.get("bom", spec)
conversion = bom.get("conversion")
if isinstance(conversion, dict):
conversion = dict(conversion)
if "target_currency" not in conversion and "currency" in conversion:
conversion["target_currency"] = conversion.get("currency")
gen = cls(
customer=bom.get("customer_name", ""),
project=bom.get("project_name", ""),
prepared_by=bom.get("prepared_by", ""),
customer=pick(bom, "customer_name", "customer"),
project=pick(bom, "project_name", "project"),
prepared_by=pick(bom, "prepared_by", "author"),
bom_date=bom.get("date", ""),
reference_label=bom.get("reference_label", ""),
reference_label=pick(bom, "reference_label", "reference"),
currency=bom.get("currency", "USD"),
realm=bom.get("realm", ""),
service_type=bom.get("service_type", ""),
conversion=bom.get("conversion"),
conversion=conversion,
notes=bom.get("notes"),
)
@@ -688,14 +728,17 @@ class OCIBomGenerator:
global_discount = bom_meta.get("discount_pct", bom.get("discount_pct", 0.0))
for item in bom.get("line_items", []):
sku = pick(item, "sku", "part_number")
if sku in ("", None):
raise ValueError("BOM line item requires 'sku' or 'part_number'")
gen.add_line_item(
sku=str(item["sku"]),
qty=item.get("qty", 0),
hours_units=item.get("hours_units"),
sku=str(sku),
qty=pick(item, "qty", "quantity", default=0),
hours_units=pick(item, "hours_units", "units", "hours", default=None),
months=item.get("months", 12),
discount=item.get("discount", global_discount),
custom_label=item.get("custom_label", ""),
custom_note=item.get("custom_note", ""),
discount=pick(item, "discount", "discount_pct", default=global_discount),
custom_label=pick(item, "custom_label", "label"),
custom_note=pick(item, "custom_note", "note", "notes"),
)
return gen

View File

@@ -17,6 +17,7 @@ Or import and use programmatically:
import yaml
import argparse
import re
from datetime import datetime
from pathlib import Path
from typing import Optional
@@ -29,6 +30,579 @@ from pptx.enum.shapes import MSO_SHAPE
from oci_pptx_base import Colors, Layouts, OraclePresBase
# ============================================================
# Defensive input helpers
# ============================================================
def pick(mapping: dict, *keys, default=""):
"""Return the first non-empty value for any of the provided keys."""
if not isinstance(mapping, dict):
return default
for key in keys:
if key in mapping:
value = mapping.get(key)
if value is not None:
return value
return default
def coerce_list(value):
"""Accept list, string, tuple, or None and normalize to a list."""
if value is None:
return []
if isinstance(value, list):
return value
if isinstance(value, tuple):
return list(value)
if isinstance(value, str):
return [value] if value else []
return [value]
def pick_list(mapping: dict, *keys):
"""Return the first list-like field normalized to a list."""
if not isinstance(mapping, dict):
return []
for key in keys:
if key in mapping:
return coerce_list(mapping.get(key))
return []
# ============================================================
# MCP flat-spec adapter
# ============================================================
def _is_flat_spec(spec: dict) -> bool:
"""MCP payload shape: no 'metadata' key, but has customer_name/id/title."""
if not isinstance(spec, dict):
return False
if "metadata" in spec:
return False
return any(k in spec for k in ("customer_name", "customer_id", "title"))
_PATTERNS_DIR = Path(__file__).resolve().parent.parent / "kb" / "patterns"
def _load_patterns_yaml(filename: str) -> dict:
"""Load a patterns YAML (supports 2-document layout: header + body)."""
path = _PATTERNS_DIR / filename
if not path.is_file():
return {}
try:
with open(path, "r") as f:
docs = [d for d in yaml.safe_load_all(f) if isinstance(d, dict)]
return docs[-1] if docs else {}
except Exception:
return {}
def _default_architecture_principles() -> dict:
"""Curated ECAL principles — 3 per category, 'applies_when: always' first."""
data = _load_patterns_yaml("architecture-principles.yaml")
principles = data.get("principles", {}) if isinstance(data, dict) else {}
result = {}
for cat in ("design", "deployment", "service"):
items = principles.get(cat, []) or []
always = [p for p in items if p.get("applies_when") == "always"]
picks = (always or items)[:3]
result[cat] = [
{
"id": p.get("id", ""),
"name": p.get("name", ""),
"summary": p.get("principle", p.get("summary", "")),
}
for p in picks
if isinstance(p, dict)
]
return result if any(result.values()) else {}
def _principles_kb_index() -> dict:
"""Index every KB principle by id so spec items with only id get enriched."""
data = _load_patterns_yaml("architecture-principles.yaml")
principles = data.get("principles", {}) if isinstance(data, dict) else {}
index = {}
for cat_items in principles.values():
for p in cat_items or []:
if isinstance(p, dict) and p.get("id"):
index[str(p["id"])] = {
"name": p.get("name", ""),
"summary": p.get("principle", p.get("summary", "")),
}
return index
def _default_operational_raci(model: str = "co_managed") -> list:
data = _load_patterns_yaml("operational-raci.yaml")
models = data.get("models", {}) if isinstance(data, dict) else {}
chosen = models.get(model) or models.get("co_managed") or {}
raci = chosen.get("raci", [])
return [r for r in raci if isinstance(r, dict)]
def _derive_environments(deployment: str, dr: str) -> list:
"""Produce a default environment catalogue from deployment model + DR flag."""
deployment_lc = (deployment or "").lower()
dr_enabled = bool(dr) and str(dr).lower() not in {"", "no", "none", "not in scope", "disabled", "false"}
envs = [
{"name": "Production", "sizing": "Full capacity per workload profile",
"isolation": "Dedicated compartment, private endpoints", "cost_pct": 55},
{"name": "Pre-Production / UAT", "sizing": "50-75% of production",
"isolation": "Shared tenancy OK, same region", "cost_pct": 20},
{"name": "Dev/Test", "sizing": "25% of production, auto-stop off-hours",
"isolation": "Shared, relaxed controls", "cost_pct": 10},
]
if dr_enabled or "multi" in deployment_lc or "dr" in deployment_lc:
envs.append({
"name": "DR (secondary region)",
"sizing": "100% of production, async replication",
"isolation": "Cross-region, same security baseline",
"cost_pct": 15,
})
return envs
_HA_DR_BY_TIER = {
"platinum": {
"technology": "RAC + cross-region Active Data Guard (sync)",
"rto": "< 1 hour",
"rpo": "< 5 minutes",
},
"gold": {
"technology": "TAC + cross-region Active Data Guard (async)",
"rto": "2-4 hours",
"rpo": "15 minutes",
},
"silver": {
"technology": "Single instance + cross-region backup restore",
"rto": "4-8 hours",
"rpo": "1 hour",
},
"bronze": {
"technology": "Single instance + Object Storage archive",
"rto": "24 hours",
"rpo": "24 hours",
},
}
_UPTIME_BY_TIER = {
"platinum": "99.99%",
"gold": "99.95%",
"silver": "99.9%",
"bronze": "99.5%",
}
def _derive_ha_dr_tiers(workloads: list) -> list:
"""Produce one HA/DR row per distinct service tier present."""
seen = []
for wl in workloads or []:
tier = str(wl.get("tier", "")).lower()
if tier in _HA_DR_BY_TIER and tier not in seen:
seen.append(tier)
return [
{"tier": t.title(), **_HA_DR_BY_TIER[t]}
for t in seen
]
def _default_security_baseline() -> dict:
"""Conservative OCI security baseline — customer confirms specifics."""
return {
"controls": {
"identity": [
"IAM compartments per environment + tag-based policies",
"Federated SSO via customer IdP (SAML/OIDC)",
"MFA enforced for all human users",
],
"network": [
"Private subnets with NSGs; no public IPs on data tier",
"Service Gateway for OCI APIs; no egress over public internet",
"FastConnect or Site-to-Site VPN for hybrid connectivity",
],
"database": [
"TDE at rest (customer-managed keys via OCI Vault)",
"Network encryption in transit (TLS 1.2+)",
"Data Safe for activity auditing + sensitive data discovery",
],
"monitoring": [
"OCI Logging + Service Connector Hub to SIEM",
"Cloud Guard detectors enabled (baseline recipe)",
"OCI Monitoring alarms on quota, cost, and security events",
],
},
"compliance": ["ISO 27001", "SOC 2", "PCI-DSS"],
}
def _derive_service_tiering(services: list) -> list:
"""Map a flat services list to service-tiering workload rows.
Preserves any tier/uptime/rto/rpo the caller supplied; fills sensible
defaults so the slide always has a full table instead of blanks.
"""
workloads = []
for svc in services or []:
if not isinstance(svc, dict):
continue
name = svc.get("name") or svc.get("workload") or svc.get("sku") or "Service"
tier = svc.get("tier") or svc.get("service_tier") or "Gold"
workloads.append({
"name": name,
"tier": str(tier).title(),
"uptime": svc.get("uptime", "99.9%"),
"rto": svc.get("rto", "4 hours"),
"rpo": svc.get("rpo", "15 minutes"),
})
return workloads
def _timeline_to_weeks(timeline: str) -> int | None:
"""Parse a simple duration phrase like '12 weeks' or '6 months'."""
if not timeline:
return None
text = str(timeline).lower()
match = re.search(r"(\d+)\s*(week|weeks|month|months)", text)
if not match:
return None
amount = int(match.group(1))
unit = match.group(2)
return amount * 4 if unit.startswith("month") else amount
def _proposal_default_migration(timeline: str = "") -> dict:
"""Generate a conservative Define/Design/Deliver plan from total duration."""
total_weeks = _timeline_to_weeks(timeline)
if total_weeks and total_weeks >= 6:
define = max(1, round(total_weeks * 0.2))
design = max(1, round(total_weeks * 0.3))
deliver = max(1, total_weeks - define - design)
total_duration = f"{total_weeks} weeks"
else:
define, design, deliver = 2, 4, 6
total_duration = timeline or "12 weeks (to validate)"
return {
"total_duration": total_duration,
"phases": [
{
"name": "Define",
"duration": f"{define} weeks",
"tasks": [
"Validate scope and business drivers",
"Baseline current estate and constraints",
"Confirm success criteria and migration assumptions",
],
},
{
"name": "Design",
"duration": f"{design} weeks",
"tasks": [
"Finalize target architecture and landing zone",
"Confirm security, DR, and networking approach",
"Validate sizing and migration wave plan",
],
},
{
"name": "Deliver",
"duration": f"{deliver} weeks",
"tasks": [
"Execute migration waves and rehearsals",
"Run cutover and stabilization",
"Complete operations handover",
],
},
],
"downtime": "Confirm cutover window with application owners during design.",
}
def _proposal_default_risks(summary: dict) -> list:
"""Infer generic-but-grounded proposal risks from sparse summary text."""
text_parts = [
pick(summary, "why"),
pick(summary, "target_state"),
pick(summary, "timeline"),
*coerce_list(summary.get("current_state")),
]
text = " ".join(str(p) for p in text_parts if p).lower()
risks = []
if any(token in text for token in ("legacy", "on-prem", "on premises", "migration")):
risks.append({
"risk": "Current-state migration scope may be larger than initially captured.",
"severity": "HIGH",
"mitigation": "Complete application and database discovery before locking the wave plan.",
})
if any(token in text for token in ("dr", "disaster recovery", "rto", "rpo", "resilien")):
risks.append({
"risk": "Resilience objectives need explicit validation against the proposed operating model.",
"severity": "MEDIUM",
"mitigation": "Confirm RTO/RPO targets and rehearse failover before production cutover.",
})
if any(token in text for token in ("cost", "budget", "tco", "byol", "payg")):
risks.append({
"risk": "Commercial assumptions may diverge from the customer's actual utilization baseline.",
"severity": "MEDIUM",
"mitigation": "Validate sizing, discount, and licensing assumptions with finance and platform owners.",
})
if any(token in text for token in ("team", "skill", "operations", "dba", "cloud")):
risks.append({
"risk": "Day-2 operating responsibilities may be unclear for the delivery team.",
"severity": "MEDIUM",
"mitigation": "Review the co-managed RACI and confirm ownership before the pilot starts.",
})
if not risks:
risks.append({
"risk": "Incomplete discovery details can force late design changes.",
"severity": "MEDIUM",
"mitigation": "Run a discovery checkpoint before committing to procurement or migration dates.",
})
return risks[:4]
def _proposal_default_next_steps(summary: dict) -> list:
timeline = pick(summary, "timeline")
steps = [
"Review the proposal with technical and business stakeholders.",
"Validate current-state inventory, sizing, and dependency assumptions.",
"Confirm the target delivery model and success criteria for the first wave.",
]
if timeline:
steps.append(f"Reconcile the plan against the stated timeline ({timeline}).")
steps.append("Decide whether to proceed with a proof of concept or detailed design workshop.")
return steps[:5]
def _enrich_partial_proposal_spec(spec: dict) -> dict:
"""Add safe fallback sections so sparse proposal specs still render usable decks."""
if not isinstance(spec, dict):
return spec
summary = spec.get("summary")
if not isinstance(summary, dict):
return spec
target_state = pick(summary, "target_state")
has_context = bool(target_state or pick(summary, "why") or coerce_list(summary.get("current_state")))
if not has_context:
return spec
enriched = dict(spec)
if "architecture" not in enriched and target_state:
enriched["architecture"] = {"description": target_state}
if "architecture_principles" not in enriched:
principles = _default_architecture_principles()
if principles:
enriched["architecture_principles"] = principles
if "migration" not in enriched:
enriched["migration"] = _proposal_default_migration(pick(summary, "timeline"))
if not any(k in enriched for k in ("operational_raci", "raci", "operations_raci")):
raci_items = _default_operational_raci("co_managed")
if raci_items:
enriched["operational_raci"] = {
"model": "co_managed",
"raci_items": raci_items,
}
if "risks" not in enriched:
enriched["risks"] = _proposal_default_risks(summary)
if "next_steps" not in enriched:
enriched["next_steps"] = {"steps": _proposal_default_next_steps(summary)}
return enriched
def _adapt_flat_spec(spec: dict) -> dict:
"""Map the MCP flat payload to the proposal-spec structure from_spec expects."""
arch = spec.get("architecture") if isinstance(spec.get("architecture"), dict) else {}
tenancy = spec.get("tenancy") if isinstance(spec.get("tenancy"), dict) else {}
customer = spec.get("customer_name") or spec.get("customer_id", "")
title = spec.get("title") or arch.get("workload", "") or "Architecture Proposal"
workload = spec.get("workload_type") or arch.get("workload", "")
deployment = spec.get("deployment_model") or arch.get("deployment", "")
billing = spec.get("billing_model") or arch.get("license_model", "")
capacity = spec.get("capacity") or arch.get("capacity") or {}
services = spec.get("services", []) or []
# Region discovery — primary + DR can arrive as flat fields, under
# architecture.*, or as a tenancy.regions list (items may be plain
# strings or {region, role} dicts).
primary_region = (
spec.get("primary_region")
or spec.get("region")
or arch.get("primary_region")
or arch.get("region")
or ""
)
dr_region = (
spec.get("dr_region")
or spec.get("secondary_region")
or arch.get("dr_region")
or arch.get("secondary_region")
or ""
)
tenancy_regions = tenancy.get("regions") or spec.get("regions") or arch.get("regions") or []
if isinstance(tenancy_regions, list):
for entry in tenancy_regions:
if isinstance(entry, dict):
name = entry.get("name") or entry.get("region") or entry.get("id") or ""
role = str(entry.get("role") or entry.get("type") or "").lower()
if role in ("primary", "hub", "prod", "production") and not primary_region:
primary_region = name
elif role in ("dr", "standby", "secondary", "failover") and not dr_region:
dr_region = name
elif isinstance(entry, str):
if not primary_region:
primary_region = entry
elif not dr_region:
dr_region = entry
region = primary_region
dr_raw = spec.get("disaster_recovery")
if dr_raw is None and "dr" in arch:
dr_raw = "Enabled" if arch.get("dr") else "Not in scope"
# If the spec names a DR region, treat DR as enabled even when the
# disaster_recovery flag is omitted.
if not dr_raw and dr_region:
dr_raw = f"Enabled — secondary region: {dr_region}"
target_parts = [p for p in (workload, deployment and f"deployment: {deployment}",
region and f"region: {region}") if p]
target_state = "".join(target_parts)
current_state = []
if isinstance(capacity, dict):
for k, v in capacity.items():
current_state.append(f"{k.replace('_', ' ').title()}: {v}")
if billing:
current_state.append(f"License model: {billing}")
if dr_raw:
current_state.append(f"Disaster recovery: {dr_raw}")
line_items = []
for svc in services:
if not isinstance(svc, dict):
continue
qty = svc.get("quantity", svc.get("qty"))
notes = svc.get("notes", "")
if qty is not None:
notes = f"Qty: {qty}" + (f"{notes}" if notes else "")
line_items.append({"component": svc.get("name", ""), "monthly_payg": "", "notes": notes})
cost_summary = spec.get("cost_summary") or {}
monthly = cost_summary.get("monthly_estimate")
if monthly is not None:
currency = cost_summary.get("currency", "USD")
annual = cost_summary.get("annual_estimate", monthly * 12)
line_items.append({
"component": "Total (estimated)",
"monthly_payg": f"{currency} {monthly:,.2f}",
"notes": f"Annual: {currency} {annual:,.2f}",
})
adapted = {
"metadata": {"customer": customer, "subtitle": title},
"summary": {
"why": f"{customer}{title}" if customer and title else (customer or title),
"current_state": current_state,
"target_state": target_state,
"timeline": "",
},
}
workloads = _derive_service_tiering(services)
if workloads:
adapted["service_tiering"] = workloads
arch_desc_parts = [p for p in (
workload and f"Workload: {workload}",
deployment and f"Deployment: {deployment}",
region and f"Primary region: {region}",
dr_region and f"DR region: {dr_region}",
billing and f"Licensing: {billing}",
) if p]
architecture_block: dict = {}
# Preserve any caller-supplied visual/diagram_path instead of overwriting.
if isinstance(arch.get("visual"), dict):
architecture_block["visual"] = arch["visual"]
if arch.get("diagram_path"):
architecture_block["diagram_path"] = arch["diagram_path"]
if arch_desc_parts:
architecture_block["description"] = "".join(arch_desc_parts)
# When we know both regions and no visual was supplied, emit a simple
# two-region visual so the DR region renders with its real name.
if region and dr_region and "visual" not in architecture_block:
architecture_block["visual"] = {
"regions": [
{"name": region, "primary": True, "label": "PRIMARY"},
{"name": dr_region, "primary": False, "label": "DR STANDBY"},
],
}
if architecture_block:
adapted["architecture"] = architecture_block
principles = _default_architecture_principles()
if principles:
adapted["architecture_principles"] = principles
ha_dr_tiers = _derive_ha_dr_tiers(workloads)
if ha_dr_tiers:
adapted["ha_dr"] = {
"tiers": ha_dr_tiers,
"description": "HA/DR posture per service tier. Confirm RTO/RPO with business stakeholders.",
}
adapted["security"] = _default_security_baseline()
environments = _derive_environments(deployment, dr_raw)
if environments:
adapted["environment_catalogue"] = {"environments": environments}
# Respect user-supplied RACI if present (string model name, list of items,
# or dict with model/raci_items); otherwise fall back to co_managed default.
user_raci = None
for key in ("operational_raci", "raci", "operations_raci"):
if spec.get(key) is not None:
user_raci = spec[key]
break
if user_raci is not None:
adapted["operational_raci"] = user_raci
else:
raci_items = _default_operational_raci("co_managed")
if raci_items:
adapted["operational_raci"] = {
"raci_items": raci_items,
"model": "co_managed",
}
adapted["next_steps"] = {
"steps": [
"Validate architecture + service tiers with technical stakeholders",
"Confirm capacity assumptions against current/expected utilization",
"Lock commitment model (PAYG vs UCM) and finalize discount",
"Define proof-of-concept / pilot scope and success criteria",
"Schedule operational handover and RACI walkthrough",
],
}
if line_items:
adapted["cost"] = {
"line_items": line_items,
"assumptions": spec.get("assumptions", []) or [],
"show_byol": False,
}
return adapted
# ============================================================
# Slide Generator
# ============================================================
@@ -445,17 +1019,23 @@ class OCIDeckGenerator(OraclePresBase):
for i, wl in enumerate(workloads):
row_idx = i + 1
bg = Colors.TABLE_ALT_ROW if row_idx % 2 == 0 else None
self._style_table_cell(table.cell(row_idx, 0), wl["name"], font_size=10, bold=True, bg_color=bg)
tier_label = wl.get("tier", "Silver")
tier_color = tier_colors.get(tier_label.lower(), Colors.SECONDARY_TEXT)
workload_name = pick(wl, "name", "workload", "title")
tier_label = pick(wl, "tier", "service_tier", default="Silver")
tier_key = tier_label.lower()
tier_color = tier_colors.get(tier_key, Colors.SECONDARY_TEXT)
tier_defaults = _HA_DR_BY_TIER.get(tier_key, {})
uptime = wl.get("uptime") or _UPTIME_BY_TIER.get(tier_key, "")
rto = wl.get("rto") or tier_defaults.get("rto", "")
rpo = wl.get("rpo") or tier_defaults.get("rpo", "")
self._style_table_cell(table.cell(row_idx, 0), workload_name, font_size=10, bold=True, bg_color=bg)
self._style_table_cell(
table.cell(row_idx, 1), tier_label.title(),
font_size=10, bold=True, color=tier_color, bg_color=bg,
alignment=PP_ALIGN.CENTER,
)
self._style_table_cell(table.cell(row_idx, 2), wl.get("uptime", ""), font_size=10, bg_color=bg, alignment=PP_ALIGN.CENTER)
self._style_table_cell(table.cell(row_idx, 3), wl.get("rto", ""), font_size=10, bg_color=bg, alignment=PP_ALIGN.CENTER)
self._style_table_cell(table.cell(row_idx, 4), wl.get("rpo", ""), font_size=10, bg_color=bg, alignment=PP_ALIGN.CENTER)
self._style_table_cell(table.cell(row_idx, 2), uptime, font_size=10, bg_color=bg, alignment=PP_ALIGN.CENTER)
self._style_table_cell(table.cell(row_idx, 3), rto, font_size=10, bg_color=bg, alignment=PP_ALIGN.CENTER)
self._style_table_cell(table.cell(row_idx, 4), rpo, font_size=10, bg_color=bg, alignment=PP_ALIGN.CENTER)
def add_architecture_principles_slide(self, principles: dict):
"""Architecture Principles slide — ECAL Design/Deployment/Service categories.
@@ -466,6 +1046,8 @@ class OCIDeckGenerator(OraclePresBase):
slide = self._add_blank_slide()
self._add_title_bar(slide, "Architecture Principles", margin=self.MARGIN)
kb_index = _principles_kb_index()
y = Inches(1.2)
col_x = self.MARGIN
col_width = Inches(4)
@@ -486,7 +1068,11 @@ class OCIDeckGenerator(OraclePresBase):
for item in items:
pid = item.get("id", "")
name = item.get("name", "")
summary = item.get("summary", "")
summary = item.get("summary", "") or item.get("principle", "")
if pid and (not name or not summary):
fallback = kb_index.get(str(pid), {})
name = name or fallback.get("name", "")
summary = summary or fallback.get("summary", "")
label = f"{pid} {name}" if pid else name
if summary:
label += f"{summary}"
@@ -533,11 +1119,11 @@ class OCIDeckGenerator(OraclePresBase):
row_idx = i + 1
bg = Colors.TABLE_ALT_ROW if row_idx % 2 == 0 else None
self._style_table_cell(
table.cell(row_idx, 0), dec["decision"],
table.cell(row_idx, 0), pick(dec, "decision", "title"),
font_size=11, bold=True, bg_color=bg,
)
self._style_table_cell(
table.cell(row_idx, 1), dec["rationale"],
table.cell(row_idx, 1), pick(dec, "rationale", "reason", "notes"),
font_size=10, bg_color=bg,
)
@@ -579,10 +1165,10 @@ class OCIDeckGenerator(OraclePresBase):
for i, tier in enumerate(tiers):
row_idx = i + 1
bg = Colors.TABLE_ALT_ROW if row_idx % 2 == 0 else None
self._style_table_cell(table.cell(row_idx, 0), tier["tier"], font_size=10, bold=True, bg_color=bg)
self._style_table_cell(table.cell(row_idx, 1), tier["technology"], font_size=10, bg_color=bg)
self._style_table_cell(table.cell(row_idx, 2), tier["rto"], font_size=10, bg_color=bg, alignment=PP_ALIGN.CENTER)
self._style_table_cell(table.cell(row_idx, 3), tier["rpo"], font_size=10, bg_color=bg, alignment=PP_ALIGN.CENTER)
self._style_table_cell(table.cell(row_idx, 0), pick(tier, "tier", "name"), font_size=10, bold=True, bg_color=bg)
self._style_table_cell(table.cell(row_idx, 1), pick(tier, "technology", "architecture", "pattern"), font_size=10, bg_color=bg)
self._style_table_cell(table.cell(row_idx, 2), pick(tier, "rto"), font_size=10, bg_color=bg, alignment=PP_ALIGN.CENTER)
self._style_table_cell(table.cell(row_idx, 3), pick(tier, "rpo"), font_size=10, bg_color=bg, alignment=PP_ALIGN.CENTER)
def add_security_slide(self, controls: dict, compliance: list = None):
"""Slide 6: Security & Compliance.
@@ -770,19 +1356,22 @@ class OCIDeckGenerator(OraclePresBase):
for i, item in enumerate(line_items):
row_idx = i + 1
is_total = "total" in item.get("component", "").lower()
component = pick(item, "component", "name", "label")
notes = pick(item, "notes", "note")
monthly_payg = pick(item, "monthly_payg", "monthly")
is_total = "total" in str(component).lower()
bg = Colors.TABLE_ALT_ROW if row_idx % 2 == 0 and not is_total else None
if is_total:
bg = Colors.TEAL
self._style_table_cell(
table.cell(row_idx, 0), item["component"],
table.cell(row_idx, 0), component,
font_size=10, bold=is_total,
color=Colors.WHITE if is_total else None,
bg_color=bg,
)
self._style_table_cell(
table.cell(row_idx, 1), item.get("monthly_payg", ""),
table.cell(row_idx, 1), monthly_payg,
font_size=10, bold=is_total,
color=Colors.WHITE if is_total else None,
bg_color=bg, alignment=PP_ALIGN.RIGHT,
@@ -795,13 +1384,13 @@ class OCIDeckGenerator(OraclePresBase):
bg_color=bg, alignment=PP_ALIGN.RIGHT,
)
self._style_table_cell(
table.cell(row_idx, 3), item.get("notes", ""),
table.cell(row_idx, 3), notes,
font_size=9, bg_color=bg,
color=Colors.WHITE if is_total else Colors.SECONDARY_TEXT,
)
else:
self._style_table_cell(
table.cell(row_idx, 2), item.get("notes", ""),
table.cell(row_idx, 2), notes,
font_size=9, bg_color=bg,
color=Colors.WHITE if is_total else Colors.SECONDARY_TEXT,
)
@@ -903,14 +1492,14 @@ class OCIDeckGenerator(OraclePresBase):
for i, phase in enumerate(phases):
color = phase_colors[i % len(phase_colors)]
duration = phase.get("duration", phase.get("weeks", ""))
milestones = phase.get("milestones", phase.get("tasks", []))
duration = pick(phase, "duration", "weeks")
milestones = pick_list(phase, "milestones", "tasks")
# Phase label (left of bar)
self._add_textbox(
slide, self.MARGIN, y,
Inches(2.3), Inches(0.4),
text=phase["name"], font_size=11, bold=True,
text=pick(phase, "name", "title"), font_size=11, bold=True,
)
# Colored phase bar with duration
@@ -1042,7 +1631,7 @@ class OCIDeckGenerator(OraclePresBase):
row_idx = i + 1
bg = Colors.TABLE_ALT_ROW if row_idx % 2 == 0 else None
self._style_table_cell(
table.cell(row_idx, 0), risk["risk"],
table.cell(row_idx, 0), pick(risk, "risk", "title"),
font_size=10, bg_color=bg,
)
sev = risk.get("severity", "MEDIUM").upper()
@@ -1053,7 +1642,7 @@ class OCIDeckGenerator(OraclePresBase):
bg_color=bg, alignment=PP_ALIGN.CENTER,
)
self._style_table_cell(
table.cell(row_idx, 2), risk["mitigation"],
table.cell(row_idx, 2), pick(risk, "mitigation", "action", "response"),
font_size=10, bg_color=bg,
)
@@ -1075,7 +1664,8 @@ class OCIDeckGenerator(OraclePresBase):
y = Inches(1.4)
for pillar in pillars:
style = status_styles.get(pillar["status"], status_styles["PASS"])
status = pick(pillar, "status", default="PASS")
style = status_styles.get(status, status_styles["PASS"])
# Status indicator pill
pill = self._add_rect(
@@ -1093,11 +1683,13 @@ class OCIDeckGenerator(OraclePresBase):
self._add_textbox(
slide, Inches(1.2), y,
Inches(4), Inches(0.45),
text=pillar["name"], font_size=13, bold=True,
text=pick(pillar, "name", "pillar"), font_size=13, bold=True,
)
# Score
score_text = f"{pillar['passed']}/{pillar['total']}" if pillar['total'] > 0 else ""
passed = pick(pillar, "passed", default=0)
total = pick(pillar, "total", default=0)
score_text = f"{passed}/{total}" if total > 0 else ""
self._add_textbox(
slide, Inches(5.5), y,
Inches(1.5), Inches(0.45),
@@ -1185,26 +1777,35 @@ class OCIDeckGenerator(OraclePresBase):
@classmethod
def from_spec(cls, spec: dict, template: Optional[str] = None) -> "OCIDeckGenerator":
"""Build a complete deck from a YAML specification."""
# The MCP server passes a flat payload shape (customer_name, services,
# cost_summary, ...) that doesn't match the proposal-spec structure this
# method was originally written against. Adapt it so the deck renders
# with actual content instead of only blank title/closing slides.
if _is_flat_spec(spec):
spec = _adapt_flat_spec(spec)
else:
spec = _enrich_partial_proposal_spec(spec)
meta = spec.get("metadata", {})
gen = cls(
customer=meta.get("customer", ""),
project=meta.get("project", ""),
architect=meta.get("architect", ""),
customer=pick(meta, "customer", "customer_name"),
project=pick(meta, "project", "project_name"),
architect=pick(meta, "architect", "prepared_by"),
firm=meta.get("firm", ""),
template=template,
)
# Slide 1: Title
gen.add_title_slide(subtitle=meta.get("subtitle", ""))
gen.add_title_slide(subtitle=pick(meta, "subtitle", "title"))
# Slide 2: Summary
if "summary" in spec:
s = spec["summary"]
gen.add_summary_slide(
why=s.get("why", ""),
current_state=s.get("current_state", []),
target_state=s.get("target_state", ""),
timeline=s.get("timeline", ""),
why=pick(s, "why"),
current_state=coerce_list(s.get("current_state")),
target_state=pick(s, "target_state"),
timeline=pick(s, "timeline"),
)
# Slide 3: Service Tiering (ECAL)
@@ -1218,10 +1819,43 @@ class OCIDeckGenerator(OraclePresBase):
# Slide 5: Architecture
if "architecture" in spec:
a = spec["architecture"]
visual = a.get("visual")
description = a.get("description", "")
primary_region = (
spec.get("primary_region")
or spec.get("region")
or a.get("primary_region")
or a.get("region")
or ""
)
dr_region = (
spec.get("dr_region")
or spec.get("secondary_region")
or a.get("dr_region")
or a.get("secondary_region")
or ""
)
# Auto-build a two-region visual when the spec names a DR region
# but didn't pre-render the visual structure itself.
if dr_region and not visual:
visual = {
"regions": [
{"name": primary_region or "Primary", "primary": True, "label": "PRIMARY"},
{"name": dr_region, "primary": False, "label": "DR STANDBY"},
],
}
elif dr_region and isinstance(visual, dict):
regions = visual.get("regions") or []
names = {str(r.get("name", "")).lower() for r in regions if isinstance(r, dict)}
if dr_region.lower() not in names:
regions.append({"name": dr_region, "primary": False, "label": "DR STANDBY"})
visual["regions"] = regions
if dr_region and description and dr_region.lower() not in description.lower():
description = f"{description} • DR region: {dr_region}"
gen.add_architecture_slide(
diagram_path=a.get("diagram_path"),
description=a.get("description", ""),
visual=a.get("visual"),
description=description,
visual=visual,
)
# Slide 6: Decisions
@@ -1256,8 +1890,8 @@ class OCIDeckGenerator(OraclePresBase):
if "cost" in spec:
c = spec["cost"]
gen.add_cost_slide(
line_items=c.get("line_items", []),
assumptions=c.get("assumptions", []),
line_items=pick_list(c, "line_items"),
assumptions=pick_list(c, "assumptions"),
show_byol=c.get("show_byol", True),
)
@@ -1274,17 +1908,38 @@ class OCIDeckGenerator(OraclePresBase):
if "migration" in spec:
m = spec["migration"]
gen.add_migration_slide(
phases=m.get("phases", []),
tools=m.get("tools", []),
downtime=m.get("downtime", ""),
phases=pick_list(m, "phases"),
tools=pick_list(m, "tools"),
downtime=pick(m, "downtime"),
)
# Slide 13: Operational RACI (ECAL)
if "operational_raci" in spec:
r = spec["operational_raci"]
raci_payload = None
for key in ("operational_raci", "raci", "operations_raci"):
if key in spec and spec[key] is not None:
raci_payload = spec[key]
break
if raci_payload is not None:
model = "co_managed"
raci_items = []
if isinstance(raci_payload, str):
model = raci_payload
elif isinstance(raci_payload, list):
raci_items = [r for r in raci_payload if isinstance(r, dict)]
elif isinstance(raci_payload, dict):
model = raci_payload.get("model") or raci_payload.get("engagement_model") or "co_managed"
items = (
raci_payload.get("raci_items")
or raci_payload.get("items")
or raci_payload.get("activities")
or []
)
raci_items = [r for r in items if isinstance(r, dict)]
if not raci_items:
raci_items = _default_operational_raci(model)
gen.add_operational_raci_slide(
raci_items=r.get("raci_items", []),
model=r.get("model", "co_managed"),
raci_items=raci_items,
model=model,
)
# Slide 14: Risks
@@ -1295,22 +1950,16 @@ class OCIDeckGenerator(OraclePresBase):
if "scorecard" in spec:
sc = spec["scorecard"]
gen.add_scorecard_slide(
pillars=sc.get("pillars", []),
recommendations=sc.get("recommendations", []),
pillars=pick_list(sc, "pillars"),
recommendations=pick_list(sc, "recommendations"),
)
# Slide 16: Next Steps
if "next_steps" in spec:
ns = spec["next_steps"]
gen.add_next_steps_slide(
steps=ns.get("steps", []),
contact_info=ns.get("contact_info", ""),
)
# Closing slides
gen.add_closing_slide(
name=meta.get("architect", ""),
title=meta.get("firm", ""),
steps=pick_list(ns, "steps", "next_steps"),
contact_info=pick(ns, "contact_info"),
)
return gen

View File

@@ -300,6 +300,8 @@ SVC_CATEGORY = {
# Integration (purple #804998)
"drg": "svc_integration",
"dynamic_routing_gateway": "svc_integration",
"rpc": "svc_integration",
"remote_peering_connection": "svc_integration",
"streaming": "svc_integration", "kafka": "svc_integration",
"queue": "svc_integration", "oci_queue": "svc_integration",
"oic": "svc_integration", "integration_cloud": "svc_integration",
@@ -408,6 +410,220 @@ OBS_TYPE_MAP = {
"auditing": "monitoring", # uses monitoring icon as closest match
}
def pick(mapping: dict, *keys, default=""):
"""Return the first non-empty value for any of the provided keys."""
if not isinstance(mapping, dict):
return default
for key in keys:
if key in mapping:
value = mapping.get(key)
if value is not None:
return value
return default
def pick_list(mapping: dict, *keys):
"""Return the first list-like field normalized to a list."""
if not isinstance(mapping, dict):
return []
for key in keys:
if key in mapping:
value = mapping.get(key)
if value is None:
return []
if isinstance(value, list):
return value
return [value]
return []
def _slugify(value: str) -> str:
text = re.sub(r"[^a-z0-9]+", "-", str(value or "").lower()).strip("-")
return text or "item"
class _SpecIdGenerator:
"""Generate stable unique ids within one normalized spec."""
def __init__(self):
self.used = set()
self.counters = {}
def make(self, prefix: str, seed: str = "") -> str:
base = f"{prefix}-{_slugify(seed)}" if seed else prefix
candidate = base
idx = 2
while candidate in self.used:
candidate = f"{base}-{idx}"
idx += 1
self.used.add(candidate)
return candidate
def _normalize_diagram_spec(spec: dict) -> dict:
"""Normalize aliases and synthesize ids before rendering."""
spec = spec or {}
id_gen = _SpecIdGenerator()
aliases = {}
def remember(node: dict, node_id: str):
for key in ("id", "name", "label", "title"):
value = node.get(key)
if value:
aliases[str(value)] = node_id
def resolve_ref(value):
if value in (None, ""):
return None
return aliases.get(str(value), str(value))
def normalize_service(node: dict, prefix: str, default_type: str = "compute") -> dict:
raw = dict(node or {})
label = pick(raw, "label", "name", "title")
node_id = pick(raw, "id", "name") or id_gen.make(prefix, label)
normalized = dict(raw)
normalized["id"] = str(node_id)
normalized["label"] = label or str(node_id)
normalized["type"] = pick(raw, "type", "service_type", default=default_type)
remember(raw, normalized["id"])
return normalized
normalized = {
"title": spec.get("title", ""),
"external": [],
"clouds": [],
"tenancy": {},
"connections": [],
}
for ext in pick_list(spec, "external"):
normalized["external"].append(normalize_service(ext, "external"))
for cloud in pick_list(spec, "clouds"):
raw_cloud = dict(cloud or {})
label = pick(raw_cloud, "label", "name", "title")
cloud_id = pick(raw_cloud, "id", "name") or id_gen.make("cloud", label)
normalized_cloud = dict(raw_cloud)
normalized_cloud["id"] = str(cloud_id)
normalized_cloud["label"] = label or str(cloud_id)
normalized_cloud["services"] = [
normalize_service(svc, "cloud-svc", default_type="compute")
for svc in pick_list(raw_cloud, "services")
]
remember(raw_cloud, normalized_cloud["id"])
normalized["clouds"].append(normalized_cloud)
tenancy_spec = dict(spec.get("tenancy", {}) or {})
normalized_tenancy = dict(tenancy_spec)
normalized_tenancy["label"] = pick(tenancy_spec, "label", "name", "title", default="Oracle Cloud Infrastructure")
normalized_tenancy["regions"] = []
for region in pick_list(tenancy_spec, "regions"):
raw_region = dict(region or {})
region_label = pick(raw_region, "label", "name", "title")
region_id = pick(raw_region, "id", "name") or id_gen.make("region", region_label)
normalized_region = dict(raw_region)
normalized_region["id"] = str(region_id)
normalized_region["label"] = region_label or str(region_id)
normalized_region["availability_domains"] = []
normalized_region["vcns"] = []
normalized_region["compartments"] = []
normalized_region["observability"] = pick_list(raw_region, "observability")
remember(raw_region, normalized_region["id"])
for ad in pick_list(raw_region, "availability_domains"):
raw_ad = dict(ad or {})
ad_label = pick(raw_ad, "label", "name", "title")
ad_id = pick(raw_ad, "id", "name") or id_gen.make("ad", ad_label)
normalized_ad = dict(raw_ad)
normalized_ad["id"] = str(ad_id)
normalized_ad["label"] = ad_label or str(ad_id)
remember(raw_ad, normalized_ad["id"])
normalized_region["availability_domains"].append(normalized_ad)
def normalize_vcn(raw_vcn: dict) -> dict:
vcn_label = pick(raw_vcn, "label", "name", "title")
vcn_id = pick(raw_vcn, "id", "name") or id_gen.make("vcn", vcn_label)
normalized_vcn = dict(raw_vcn)
normalized_vcn["id"] = str(vcn_id)
normalized_vcn["label"] = vcn_label or str(vcn_id)
parent_ref = pick(raw_vcn, "parent", default="")
if parent_ref:
normalized_vcn["parent"] = resolve_ref(parent_ref) or str(parent_ref)
normalized_vcn["gateways"] = [
normalize_service(gw, "gw", default_type="drg" if pick(gw, "type", "service_type") in ("drg", "dynamic_routing_gateway") else "compute")
for gw in pick_list(raw_vcn, "gateways")
]
normalized_vcn["subnets"] = []
remember(raw_vcn, normalized_vcn["id"])
for subnet in pick_list(raw_vcn, "subnets"):
raw_subnet = dict(subnet or {})
subnet_label = pick(raw_subnet, "label", "name", "title")
subnet_id = pick(raw_subnet, "id", "name") or id_gen.make("subnet", subnet_label)
normalized_subnet = dict(raw_subnet)
normalized_subnet["id"] = str(subnet_id)
normalized_subnet["label"] = subnet_label or str(subnet_id)
normalized_subnet["services"] = [
normalize_service(svc, "svc", default_type="compute")
for svc in pick_list(raw_subnet, "services")
]
remember(raw_subnet, normalized_subnet["id"])
normalized_vcn["subnets"].append(normalized_subnet)
return normalized_vcn
for vcn in pick_list(raw_region, "vcns"):
normalized_region["vcns"].append(normalize_vcn(dict(vcn or {})))
for comp in pick_list(raw_region, "compartments"):
raw_comp = dict(comp or {})
comp_label = pick(raw_comp, "label", "name", "title")
comp_id = pick(raw_comp, "id", "name") or id_gen.make("compartment", comp_label)
normalized_comp = dict(raw_comp)
normalized_comp["id"] = str(comp_id)
normalized_comp["label"] = comp_label or str(comp_id)
normalized_comp["vcns"] = [
normalize_vcn(dict(vcn or {}))
for vcn in pick_list(raw_comp, "vcns")
]
remember(raw_comp, normalized_comp["id"])
normalized_region["compartments"].append(normalized_comp)
normalized_tenancy["regions"].append(normalized_region)
normalized["tenancy"] = normalized_tenancy
onprem_spec = spec.get("onprem")
if not onprem_spec:
onprem_spec = spec.get("on_prem")
if onprem_spec:
raw_onprem = dict(onprem_spec or {})
onprem_label = pick(raw_onprem, "label", "name", "title", default="On-Premises Data Center")
normalized_onprem = dict(raw_onprem)
normalized_onprem["id"] = pick(raw_onprem, "id", "name", default="onprem") or "onprem"
normalized_onprem["label"] = onprem_label
normalized_onprem["services"] = [
normalize_service(svc, "onprem-svc", default_type="compute")
for svc in pick_list(raw_onprem, "services")
]
remember(raw_onprem, normalized_onprem["id"])
normalized["onprem"] = normalized_onprem
for conn in pick_list(spec, "connections"):
raw_conn = dict(conn or {})
source = resolve_ref(pick(raw_conn, "from", "source", default=None))
target = resolve_ref(pick(raw_conn, "to", "target", default=None))
if not source or not target:
continue
normalized_conn = dict(raw_conn)
normalized_conn["from"] = source
normalized_conn["to"] = target
normalized_conn["type"] = pick(raw_conn, "type", "service_type", default="standard")
normalized["connections"].append(normalized_conn)
return normalized
# Unicode circled numbers for flow badges ① ② ③ ... ⑳
_CIRCLED_NUMS = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳"
@@ -1314,6 +1530,7 @@ class OCIDiagramGenerator:
Plus optional on-premises, external actors, and connections.
See examples/diagram-spec.yaml for the full format.
"""
spec = _normalize_diagram_spec(spec)
gen = cls()
# External actors (users, internet, third-party)
@@ -1425,6 +1642,7 @@ class OCIDiagramGenerator:
# Regions — auto-size containers from content when spec doesn't override
region_x_cursor = 15 # tracks right edge for auto-positioning next region
drg_by_region = [] # list of (region_id, drg_id) for cross-region RPC auto-wiring
for region in tenancy_spec.get("regions", []):
is_primary = region.get("primary", False)
rx = region.get("x", region_x_cursor)
@@ -1485,6 +1703,18 @@ class OCIDiagramGenerator:
auto_rw = total_vcn_w + drg_lane_w + 50 # margins
auto_rh = total_vcn_h + 70 # 45 top (label) + 25 bottom
# Local DR standby (same region as primary) — adds a dormant
# node below the VCNs. Bump auto-height to make room.
local_dr = bool(region.get("local_dr") or region.get("local_dr_standby"))
if not local_dr:
for v in region_vcns:
if v.get("local_dr") or v.get("local_dr_standby"):
local_dr = True
break
local_dr_h = 90 if local_dr else 0
if local_dr:
auto_rh += local_dr_h
# Always use at least auto-calculated size even if spec is smaller
rw = max(region.get("w", auto_rw), auto_rw)
rh = max(region.get("h", auto_rh), auto_rh)
@@ -1523,6 +1753,7 @@ class OCIDiagramGenerator:
drg_id, label, drg["type"],
region["id"], drg_x, drg_y, drg_w, drg_h,
)
drg_by_region.append((region["id"], drg_id))
# VCNs — offset right if DRG lane present
vcn_offset_x = drg_lane_w + 15
@@ -1617,6 +1848,25 @@ class OCIDiagramGenerator:
subnet_y += sh + SUBNET_GAP
# Local DR standby — render a dormant node inside the region
# at the bottom-right corner so it's visually clear that the
# primary has a same-region failover target.
if local_dr:
standby_w = 160
standby_h = 70
standby_x = max(15, rw - standby_w - 20)
standby_y = rh - standby_h - 15
standby_label = (
region.get("local_dr_label")
or region.get("local_dr_standby_label")
or "Local DR\nStandby"
)
standby_id = region.get("local_dr_id") or gen._next_id()
gen.add_service(
standby_id, standby_label, "standby",
region["id"], standby_x, standby_y, standby_w, standby_h,
)
# Compartments inside region — can contain VCNs
for comp in region.get("compartments", []):
comp_id = comp.get("id", gen._next_id())
@@ -1724,9 +1974,39 @@ class OCIDiagramGenerator:
)
opx += svc_w + 12
# Cross-region RPC — when 2+ regions own a DRG and the spec didn't
# already wire a peering link between them, auto-add a "Remote
# Peering Connection" edge for each pair so the cross-region path
# is visible in the diagram.
connections = spec.get("connections", [])
if len(drg_by_region) >= 2:
existing_pairs = {
(c.get("from"), c.get("to")) for c in connections
if isinstance(c, dict)
}
existing_pairs |= {(b, a) for (a, b) in existing_pairs}
seen_drgs_per_region = {}
for region_id, drg_id in drg_by_region:
seen_drgs_per_region.setdefault(region_id, []).append(drg_id)
region_ids = list(seen_drgs_per_region.keys())
for i in range(len(region_ids)):
for j in range(i + 1, len(region_ids)):
src = seen_drgs_per_region[region_ids[i]][0]
tgt = seen_drgs_per_region[region_ids[j]][0]
if (src, tgt) in existing_pairs:
continue
connections = list(connections) + [{
"id": f"rpc_{src}_{tgt}",
"from": src,
"to": tgt,
"type": "network",
"label": "RPC\n(Remote Peering)",
}]
existing_pairs.add((src, tgt))
existing_pairs.add((tgt, src))
# Connections — merge duplicate from/to pairs (e.g., dual FastConnect)
# into a single edge with combined label
connections = spec.get("connections", [])
seen_pairs = {} # (from, to) -> index in deduped list
deduped = []
for conn in connections:

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,104 @@ 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)
return 0
def inspect_sku(part_number):
"""Fetch and display a single SKU from the API."""
@@ -571,6 +676,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 +697,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: