The OCI command-line interface (oci) and the Exadata/DB System management CLI (dbaascli) are the tools of choice for automating Oracle database operations on OCI. This article covers real-world scenarios with complete, runnable commands — including the complex case of creating a Data Guard standby when the primary database uses Oracle Key Vault (OKV) for TDE key management.
Reference Environment
All commands use the following environment:
Region: eu-frankfurt-1
Compartment: prod-compartment (OCID: ocid1.compartment.oc1..aaaaxxxx)
Primary DB System: prod-db-system
Hostname: proddb01.prod.subnet.vcn.oraclevcn.com
Database: PRODCDB (CDB + PDB: PDB_PROD)
Shape: VM.Standard.E4.Flex (4 OCPUs, 60 GB RAM)
DB Version: 19.22.0.0
DB Home OCID: ocid1.dbhome.oc1.eu-frankfurt-1.aaaaxxxx
Database OCID: ocid1.database.oc1.eu-frankfurt-1.aaaaxxxx
Standby DB System: standby-db-system (to be created)
Hostname: stbydb01.stby.subnet.vcn.oraclevcn.com
AD: AD-2 (primary is AD-1)
OKV Server: okv01.prod.example.com (port 5695)
OKV Wallet: /etc/oracle/okv/ (on each DB System compute node)
Jump/Admin Host: admin01.prod.example.com
OCI CLI version: 3.x.x
Python: 3.9
Part 1: OCI-CLI — Foundation Commands
Install and Configure OCI-CLI
# [[email protected] ~]
# Install OCI CLI
bash -c "$(curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh)"
# Configure with API key authentication
oci setup config
# Prompts:
# User OCID: ocid1.user.oc1..aaaaxxxx
# Tenancy OCID: ocid1.tenancy.oc1..aaaaxxxx
# Region: eu-frankfurt-1
# Generate new API key pair: Y
# Key file location: ~/.oci/oci_api_key.pem
# Upload the generated public key to OCI Console: Identity → Users → API Keys → Add
Useful OCI-CLI Shortcut: Environment Variables
# [[email protected] ~]
# Set frequently used OCIDs as environment variables
export OCI_COMPARTMENT=ocid1.compartment.oc1..aaaaxxxx
export OCI_PRIMARY_DB_SYSTEM=ocid1.dbsystem.oc1.eu-frankfurt-1.aaaaxxxx
export OCI_PRIMARY_DB=ocid1.database.oc1.eu-frankfurt-1.aaaaxxxx
export OCI_PRIMARY_DB_HOME=ocid1.dbhome.oc1.eu-frankfurt-1.aaaaxxxx
export OCI_REGION=eu-frankfurt-1
# Also useful: set output format to table for human-readable output
export OCI_CLI_TABLE_OUTPUT_STYLE=table
Part 2: Creating a Data Guard Standby via OCI-CLI
Step 1: Verify Primary Database Status
# [[email protected] ~]
oci db database get \
--database-id $OCI_PRIMARY_DB \
--query 'data.{Name:"db-name", State:"lifecycle-state", "DB Unique Name":"db-unique-name", Version:"db-version"}' \
--output table
+-------------------+--------+--------------------+-----------+
| DB Unique Name | Name | State | Version |
+-------------------+--------+--------------------+-----------+
| PRODCDB | PRODCDB| AVAILABLE | 19.22.0.0 |
+-------------------+--------+--------------------+-----------+
Step 2: Get the Subnet OCID for the Standby AD
# [[email protected] ~]
# Find the standby subnet in AD-2
oci network subnet list \
--compartment-id $OCI_COMPARTMENT \
--query 'data[?contains("display-name",`stby`)].{Name:"display-name", OCID:id, AD:"availability-domain"}' \
--output table
Step 3: Create the Data Guard Association (New DB System)
This single command provisions the standby DB System, configures redo transport, and establishes Data Guard:
# [[email protected] ~]
oci db data-guard-association create with-new-db-system \
--database-id $OCI_PRIMARY_DB \
--creation-type "NewDbSystem" \
--database-admin-password "StrongPass1#2026" \
--protection-mode "MAXIMUM_AVAILABILITY" \
--transport-type "SYNC" \
--display-name "PRODCDB-Standby" \
--hostname "stbydb01" \
--shape "VM.Standard.E4.Flex" \
--cpu-core-count 4 \
--data-storage-size-in-gbs 512 \
--availability-domain "cKmO:EU-FRANKFURT-1-AD-2" \
--subnet-id "ocid1.subnet.oc1.eu-frankfurt-1.aaaaxxxx-stby" \
--node-count 1 \
--time-zone "Europe/Berlin" \
--license-model "LICENSE_INCLUDED" \
--is-active-data-guard-enabled true \
--peer-db-unique-name "PRODCDB_STBY"
Step 4: Monitor Standby Provisioning
Provisioning takes 20–40 minutes. Poll until lifecycle-state is AVAILABLE:
# [[email protected] ~]
# Get the Data Guard Association OCID from the output of the previous command
DG_ASSOC_OCID="ocid1.dgassociation.oc1.eu-frankfurt-1.aaaaxxxx"
watch -n 30 "oci db data-guard-association get \
--database-id $OCI_PRIMARY_DB \
--data-guard-association-id $DG_ASSOC_OCID \
--query 'data.{Status:\"lifecycle-state\", \"DG Role\":role, \"Peer DB\":\"peer-db-unique-name\", \"Lag Secs\":\"apply-lag\"}' \
--output table"
+-----------+----------+------------------+-----------+
| DG Role | Lag Secs | Peer DB | Status |
+-----------+----------+------------------+-----------+
| PRIMARY | 0 | PRODCDB_STBY | AVAILABLE |
+-----------+----------+------------------+-----------+
Part 3: Data Guard Standby Creation When Primary Uses OKV
When the primary database uses Oracle Key Vault (OKV) for TDE master key management, the standby must be enrolled with the same OKV server before redo apply can start. Without OKV connectivity, the standby cannot decrypt redo containing encrypted tablespace data.
Architecture with OKV
Primary (proddb01) ──SYNC redo──► Standby (stbydb01)
│ │
└──────────── OKV ─────────────────►└── (same OKV server/wallet)
okv01.prod.example.com:5695
Step 1: Install OKV Client on the Standby Compute Node
The OCI-provisioned standby compute node does not automatically have the OKV client installed. Connect to the standby via SSH:
# [[email protected] ~]
# Find the standby DB System's compute node public/private IP
oci db db-node list \
--db-system-id "ocid1.dbsystem.oc1.eu-frankfurt-1.aaaaxxxx-stby" \
--query 'data[0].{Hostname:"hostname", "Private IP":"private-ip-address", State:"lifecycle-state"}' \
--output table
# SSH to the standby node
ssh [email protected]
# [[email protected] ~]
# Switch to root and install OKV client
sudo su -
# Download OKV client from OKV server (or from internal repository)
# The okvutil binary comes from the OKV client install package
# Transfer from admin host if needed:
scp [email protected]:/u01/software/okv_client_21.9.zip /tmp/
unzip /tmp/okv_client_21.9.zip -d /opt/okv_install/
cd /opt/okv_install/
./install.sh
# Follow prompts:
# OKV Server: okv01.prod.example.com
# OKV Port: 5695
# Install dir: /etc/oracle/okv
Step 2: Enrol the Standby with OKV
# [[email protected] ~]
# Enrol the standby database endpoint with OKV
okvutil enroll \
--okv-server okv01.prod.example.com:5695 \
--okv-endpoint stbydb01-prodcdb \
--password <okv_admin_password>
Step 3: Configure sqlnet.ora and Wallet on the Standby
# [[email protected] ~]
# Configure sqlnet.ora to use OKV keystore
cat >> $ORACLE_HOME/network/admin/sqlnet.ora << 'EOF'
ENCRYPTION_WALLET_LOCATION =
(SOURCE =
(METHOD = OKV)
(METHOD_DATA =
(DIRECTORY = /etc/oracle/okv)))
EOF
Step 4: Open the Keystore on the Standby
-- [[email protected] ~]
-- Connect to the standby as SYSDBA
sqlplus / as sysdba
-- Open the OKV keystore on the standby (mount mode — before redo apply)
ADMINISTER KEY MANAGEMENT SET KEYSTORE OPEN
IDENTIFIED BY EXTERNAL STORE
CONTAINER = ALL;
-- Verify keystore is open
SELECT wrl_type, wrl_parameter, status, wallet_type, con_id
FROM v$encryption_wallet;
WRL_TYPE WRL_PARAMETER STATUS WALLET_TYPE CON_ID
-------- -------------------- ------- ------------ ------
OKV /etc/oracle/okv/ OPEN OKV 0
OKV /etc/oracle/okv/ OPEN OKV 1
OKV /etc/oracle/okv/ OPEN OKV 3
Step 5: Start Managed Recovery on the Standby
-- [[email protected] ~] as sysdba
-- Now that OKV is open, start redo apply
ALTER DATABASE RECOVER MANAGED STANDBY DATABASE USING CURRENT LOGFILE DISCONNECT FROM SESSION;
-- Verify apply is running
SELECT process, status, thread#, sequence#
FROM v$managed_standby
ORDER BY process;
Step 6: Automate OKV Keystore Open on Standby Startup
Add an OKV keystore open to the standby startup trigger to ensure it always opens automatically after a DB restart:
-- Create a STARTUP trigger to open OKV keystore automatically
CREATE OR REPLACE TRIGGER open_okv_on_startup
AFTER STARTUP ON DATABASE
BEGIN
EXECUTE IMMEDIATE 'ADMINISTER KEY MANAGEMENT SET KEYSTORE OPEN
IDENTIFIED BY EXTERNAL STORE CONTAINER = ALL';
END;
/
Part 4: dbaascli — OCI DB System Management CLI
dbaascli runs on the OCI DB System compute node itself (not on the admin host). It manages database lifecycle operations — patching, backup, switchover — at the OS level.
Connect to the DB System
# [[email protected] ~]
ssh [email protected]
sudo su - oracle
Check Database and DB System Status
# [[email protected] ~]
dbaascli database list
{
"databases": [
{
"uniqueName": "PRODCDB",
"status": "RUNNING",
"oracleHome": "/u01/app/oracle/product/19.0.0/dbhome_1",
"version": "19.22.0.0"
}
]
}
Apply a Database Patch via dbaascli
# [[email protected] ~]
# List available patches for the current DB Home
dbaascli dbhome queryPatches --dbHomeDirectoryPath /u01/app/oracle/product/19.0.0/dbhome_1
# Apply the January 2026 Release Update
dbaascli dbhome patch \
--dbHomeDirectoryPath /u01/app/oracle/product/19.0.0/dbhome_1 \
--patchId 37056207 \
--action apply \
--dbName PRODCDB
# Monitor patch progress
dbaascli job show --jobid <job_id_from_previous_output>
Backup a Database via dbaascli
# [[email protected] ~]
# Create an on-demand backup to OCI Object Storage
dbaascli database backup \
--dbName PRODCDB \
--backupType INCREMENTAL \
--tag "MANUAL_BACKUP_$(date +%Y%m%d)"
# List recent backups
dbaascli database listBackups --dbName PRODCDB
Data Guard Switchover via dbaascli
# [[email protected] ~]
# Perform a planned switchover (primary → standby)
dbaascli database switchover \
--dbName PRODCDB \
--targetDbName PRODCDB_STBY
# Monitor switchover status
dbaascli job show --jobid <switchover_job_id>
Part 5: Useful Automation Scripts
Script 1: Daily Data Guard Lag Report
#!/bin/bash
# [[email protected] ~]
# File: /u01/scripts/dg_lag_report.sh
# Purpose: Query Data Guard associations and report lag to Slack/email
OCI_DB_ID="ocid1.database.oc1.eu-frankfurt-1.aaaaxxxx"
DG_ASSOC_ID="ocid1.dgassociation.oc1.eu-frankfurt-1.aaaaxxxx"
REPORT_DATE=$(date '+%Y-%m-%d %H:%M:%S')
echo "=== Data Guard Lag Report: $REPORT_DATE ==="
oci db data-guard-association get \
--database-id "$OCI_DB_ID" \
--data-guard-association-id "$DG_ASSOC_ID" \
--query 'data.{"Apply Lag":"apply-lag",
"Transport Lag":"transport-lag",
"Status":"lifecycle-state",
"Role":role,
"Peer Role":"peer-role",
"Protection Mode":"protection-mode"}' \
--output table
# Alert if apply lag > 5 minutes
LAG=$(oci db data-guard-association get \
--database-id "$OCI_DB_ID" \
--data-guard-association-id "$DG_ASSOC_ID" \
--query 'data."apply-lag"' \
--raw-output)
if [[ "$LAG" > "+00 00:05:00" ]]; then
echo "WARNING: Apply lag ($LAG) exceeds 5 minutes!" | \
mail -s "ALERT: DG Lag Warning - proddb01" [email protected]
fi
# Schedule as cron job on admin01:
crontab -e
# Add:
# */15 * * * * /u01/scripts/dg_lag_report.sh >> /u01/logs/dg_lag.log 2>&1
Script 2: OCI DB System Health Check
#!/bin/bash
# [[email protected] ~]
# File: /u01/scripts/oci_db_health.sh
# Purpose: Quick health snapshot of all DB Systems in a compartment
COMPARTMENT="ocid1.compartment.oc1..aaaaxxxx"
echo "=== OCI Database System Health: $(date) ==="
echo ""
echo "-- DB Systems --"
oci db system list \
--compartment-id "$COMPARTMENT" \
--query 'data[*].{"Name":"display-name", "Shape":shape, "State":"lifecycle-state", "Nodes":"node-count"}' \
--output table
echo ""
echo "-- Databases --"
oci db database list \
--compartment-id "$COMPARTMENT" \
--query 'data[*].{"DB Name":"db-name", "Unique Name":"db-unique-name", "State":"lifecycle-state", "Version":"db-version"}' \
--output table
echo ""
echo "-- Work Requests (last 24h) --"
oci work-requests work-request list \
--compartment-id "$COMPARTMENT" \
--query 'data[?contains(`["ACCEPTED","IN_PROGRESS","CANCELING"]`, status)].{"Operation":"operation-type", "Status":status, "Percent":("percent-complete"|to_string(@))}' \
--output table
Script 3: Automated Patch Application with Pre/Post Checks
#!/bin/bash
# [[email protected] ~]
# File: /u01/scripts/apply_patch.sh
# Usage: ./apply_patch.sh <patch_id> <dbhome_path> <db_name>
PATCH_ID=$1
DBHOME=$2
DB_NAME=$3
LOG_DIR="/u01/logs/patching"
LOG_FILE="$LOG_DIR/patch_${PATCH_ID}_$(date +%Y%m%d_%H%M%S).log"
mkdir -p "$LOG_DIR"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"; }
log "=== Starting patch $PATCH_ID for $DB_NAME ==="
log "DB Home: $DBHOME"
# Pre-patch: capture baseline performance metrics
log "Capturing pre-patch AWR snapshot..."
sqlplus -S / as sysdba << EOF >> "$LOG_FILE"
EXEC DBMS_WORKLOAD_REPOSITORY.CREATE_SNAPSHOT();
SELECT snap_id, begin_interval_time FROM dba_hist_snapshot WHERE rownum = 1 ORDER BY snap_id DESC;
EXIT;
EOF
# Pre-patch: validate database
log "Running pre-patch database validation..."
dbaascli database precheck \
--dbName "$DB_NAME" >> "$LOG_FILE" 2>&1
if [ $? -ne 0 ]; then
log "ERROR: Pre-patch validation failed. Aborting."
exit 1
fi
# Apply patch
log "Applying patch $PATCH_ID..."
dbaascli dbhome patch \
--dbHomeDirectoryPath "$DBHOME" \
--patchId "$PATCH_ID" \
--action apply \
--dbName "$DB_NAME" >> "$LOG_FILE" 2>&1
PATCH_STATUS=$?
log "Patch exit code: $PATCH_STATUS"
# Post-patch: AWR snapshot
log "Capturing post-patch AWR snapshot..."
sqlplus -S / as sysdba << EOF >> "$LOG_FILE"
EXEC DBMS_WORKLOAD_REPOSITORY.CREATE_SNAPSHOT();
EXIT;
EOF
# Post-patch: check for invalid objects
log "Checking for invalid objects post-patch..."
sqlplus -S / as sysdba << EOF | tee -a "$LOG_FILE"
SET PAGESIZE 100 LINESIZE 200
SELECT owner, object_name, object_type, status
FROM dba_objects
WHERE status = 'INVALID'
AND owner NOT IN ('SYS','SYSTEM','DBSNMP','XDB')
ORDER BY owner, object_name;
EXIT;
EOF
log "=== Patch $PATCH_ID completed. Log: $LOG_FILE ==="
# Email report
if command -v mail &>/dev/null; then
mail -s "Patch $PATCH_ID Applied to $DB_NAME" \
[email protected] < "$LOG_FILE"
fi
Script 4: OKV Keystore Status Check Across All PDBs
# [[email protected] ~]
# Quick OKV wallet status across all containers
sqlplus -S / as sysdba << 'EOF'
SET LINESIZE 120 PAGESIZE 50
COLUMN wrl_type FORMAT A10
COLUMN wrl_parameter FORMAT A30
COLUMN status FORMAT A10
COLUMN wallet_type FORMAT A12
COLUMN con_id FORMAT 999
SELECT wrl_type,
wrl_parameter,
status,
wallet_type,
con_id
FROM v\$encryption_wallet
ORDER BY con_id;
-- Alert if any container is not OPEN
SELECT 'WARNING: Container ' || con_id || ' wallet status is ' || status AS alert
FROM v\$encryption_wallet
WHERE status != 'OPEN';
EOF
Script 5: List All DB Systems and Their Data Guard Associations
#!/bin/bash
# [[email protected] ~]
# File: /u01/scripts/dg_inventory.sh
# Purpose: Full Data Guard topology report
COMPARTMENT="ocid1.compartment.oc1..aaaaxxxx"
echo "=== Data Guard Topology Report: $(date) ==="
# Get all databases
DB_IDS=$(oci db database list \
--compartment-id "$COMPARTMENT" \
--query 'data[*].id' \
--raw-output | tr -d '[]"' | tr ',' '\n')
for DB_ID in $DB_IDS; do
DB_NAME=$(oci db database get --database-id "$DB_ID" --query 'data."db-unique-name"' --raw-output 2>/dev/null)
# Check for DG associations
DG_ASSOCS=$(oci db data-guard-association list \
--database-id "$DB_ID" \
--query 'data[*].{Role:role,"Peer DB":"peer-db-unique-name","Protection Mode":"protection-mode","Apply Lag":"apply-lag",Status:"lifecycle-state"}' \
--output table 2>/dev/null)
if [ -n "$DG_ASSOCS" ] && echo "$DG_ASSOCS" | grep -q "PRIMARY\|STANDBY"; then
echo ""
echo "Database: $DB_NAME"
echo "$DG_ASSOCS"
fi
done
Part 6: OCI CLI for Backup Management
Create and Manage Manual Backups
# [[email protected] ~]
# Create on-demand backup
oci db backup create \
--database-id $OCI_PRIMARY_DB \
--display-name "pre-patch-backup-$(date +%Y%m%d)" \
--type FULL
# List last 5 backups
oci db backup list \
--database-id $OCI_PRIMARY_DB \
--sort-by TIMECREATED \
--sort-order DESC \
--limit 5 \
--query 'data[*].{"Backup Name":"display-name","Type":type,"State":"lifecycle-state","Time":"time-ended","Size GB":"database-size-in-gbs"}' \
--output table
Restore from Backup (Point-in-Time Recovery)
# [[email protected] ~]
# Restore database to a specific timestamp (point-in-time recovery)
oci db database restore \
--database-id $OCI_PRIMARY_DB \
--timestamp "2026-03-01T08:00:00.000Z" \
--latest false