Files
ROMFASTSQL/proxmox/lxc108-oracle/roa-windows-setup/scripts/lib/oracle-functions.ps1
Marius a5ea8c7ffa fix(oracle): instanta citeste TNS_ADMIN-ul de masina, nu Oracle Home
Continuarea corectiei 3 din ec66377. Aceea rezolva doar jumatatea vizibila a
problemei; jumatatea ascunsa ar fi lovit la primul reboot al serverului, dupa
plecarea de la client. Gasit la VADECO, verificat pe loc.

1. ORA-12514 la toti clientii, cu baza perfect sanatoasa.

   TNS_ADMIN de masina nu decide doar de unde se citeste sqlnet.ora: instanta
   rezolva de acolo si aliasurile din tnsnames.ora. La Oracle XE LOCAL_LISTENER
   e implicit aliasul LISTENER_XE, definit doar in tnsnames.ora din Oracle Home.
   Cand serviciul reporneste dupa ce ROAClient a pus TNS_ADMIN pe folderul
   instantclient-ului, aliasul nu se mai rezolva, instanta nu se mai
   inregistreaza la listener, si atunci: v$instance OPEN, v$pdbs READ WRITE,
   listener pornit pe 0.0.0.0:1521, lsnrctl arata doar CLRExtProc, si absolut
   orice client primeste ORA-12514.

   Capcana e de timp, nu de continut: o instanta pornita inainte ca TNS_ADMIN
   sa existe nu il vede, deci instalarea pare impecabila si cade la primul
   reboot. Reprodus la VADECO cu un simplu Restart-Service OracleServiceXE.

   Pasul 01 scrie acum si tnsnames.ora in TNS_ADMIN (aliasul ROA pentru
   ROAClient - sablonul livrat cu el arata spre HOST=SERVER_ROA, inexistent -
   plus LISTENER_XE pentru instanta) si, mai important, fixeaza LOCAL_LISTENER
   pe adresa literala. Doar asta din urma rezista si daca ROAClient se
   instaleaza dupa kit, sau daca folderul instantclient se muta.

2. ORA-01017 desi parola e corecta.

   Cu SQLNET.ALLOWED_LOGON_VERSION_SERVER=8 serverul autentifica orice client
   pre-12c pe baza verificatorului 10G. Un cont fara 10G in PASSWORD_VERSIONS
   nu se mai poate conecta din Instant Client 10/11, si eroarea nu e ORA-28040,
   ci ORA-01017 - fix cea care trimite pe pista parolei. Dupa o instalare 21c,
   SYSTEM are doar 11G 12C; CONTAFIN_ORACLE scapa doar pentru ca pasul 01 ii
   rescrie parola dupa ce a scris sqlnet.ora. Pasul 01 rescrie acum si parola
   lui SYSTEM, din CDB$ROOT, cu aceeasi valoare.

   Subtilitate: interogat din PDB, PASSWORD_VERSIONS al unui utilizator comun
   ramane cel vechi si dupa reparatie, desi autentificarea foloseste definitia
   din root si functioneaza. Coloana nu e o dovada pentru SYSTEM; conectarea e.

3. Pasul 07 verifica acum ce nu se vede pe hartie.

   Toate cele trei erori trec de verificarile de pana acum: fisierul exista,
   parametrul e setat, contul e OPEN, obiectele sunt valide. Sectiunea noua
   "Conectivitate client vechi (Instant Client)" verifica unde se citeste
   efectiv sqlnet.ora, daca tnsnames.ora din TNS_ADMIN mai e sablonul, daca
   LOCAL_LISTENER e alias sau adresa, ce verificatoare de parola au conturile,
   si - singurul lucru care le prinde pe toate - face o conectare adevarata cu
   Instant Client-ul gasit pe masina. La esec, raportul spune si unde sa te
   uiti. Parametri noi: -ContafinPassword, -InstantClientDir.

4. Get-ServerLanIPv4, in biblioteca.

   Pe serverele la care intram prin Tailscale, un tnsnames.ora cu 100.x nu duce
   nicaieri de pe o statie. Functia alege IP-ul din LAN sarind peste Tailscale
   (100.64.0.0/10) si APIPA, dupa ruta implicita cand sunt mai multe placi.
   Get-ListenerHost o foloseste si el la fallback-ul pe 0.0.0.0, unde inainte
   lua prima adresa nefiltrata.

La VADECO a mai fost nevoie de o corectie manuala, in afara kitului: listener.ora
avea HOST = <nume>.ts.net, deci pornirea listenerului depindea de Tailscale.
Regula generala e in documentatie.

Detaliile, cu dovezi si comenzi de diagnostic: docs/lectii-conectare-client-oracle.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzF1hf4aFS1tmJWpPMiwGp
2026-08-25 22:14:45 +03:00

1231 lines
34 KiB
PowerShell

#Requires -Version 5.1
<#
.SYNOPSIS
Oracle connectivity and operations functions for ROA setup scripts.
.DESCRIPTION
Provides functions for Oracle database connectivity, SQL*Plus execution,
version detection, and Data Pump operations.
.NOTES
File Name : oracle-functions.ps1
Prerequisite : PowerShell 5.1 or higher, Oracle Client installed
Copyright 2024 : ROMFAST
#>
# Source logging functions
. "$PSScriptRoot\logging-functions.ps1"
<#
.SYNOPSIS
IP-ul din LAN al serverului - cel pe care il vad statiile de lucru.
.DESCRIPTION
Alege adresa IPv4 a interfetei care are ruta implicita, sarind peste:
- loopback (127.x) si APIPA (169.254.x);
- intervalul Tailscale (100.64.0.0/10). Pe serverele la care intram prin
Tailscale, adresa 100.x exista si e perfect functionala pentru noi, dar
e inutila intr-un tnsnames.ora dus pe statii: statia nu e in tailnet.
Daca nu gaseste nimic potrivit, intoarce sirul gol - apelantul decide.
.OUTPUTS
String. Adresa IPv4 din LAN, sau "" daca nu poate fi determinata.
.EXAMPLE
$ip = Get-ServerLanIPv4
#>
function Get-ServerLanIPv4 {
[CmdletBinding()]
param()
$eTailscale = { param($ip) $ip -match '^100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.' }
$adrese = @()
try {
$adrese = @(Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop |
Where-Object {
$_.IPAddress -notmatch '^(127\.|169\.254\.)' -and
-not (& $eTailscale $_.IPAddress)
})
}
catch {
# Masini fara modulul NetTCPIP: cadem pe rezolvarea numelui propriu.
$ipuri = @([Net.Dns]::GetHostAddresses([Net.Dns]::GetHostName()) |
Where-Object { $_.AddressFamily -eq 'InterNetwork' } |
ForEach-Object { $_.IPAddressToString } |
Where-Object { $_ -notmatch '^(127\.|169\.254\.)' -and -not (& $eTailscale $_) })
if ($ipuri.Count -gt 0) { return $ipuri[0] }
return ""
}
if ($adrese.Count -eq 0) { return "" }
if ($adrese.Count -eq 1) { return $adrese[0].IPAddress }
# Mai multe placi: o alegem pe cea cu ruta implicita.
try {
$rute = @(Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction Stop |
Sort-Object RouteMetric, ifMetric)
foreach ($r in $rute) {
$potrivire = $adrese | Where-Object { $_.InterfaceIndex -eq $r.ifIndex } | Select-Object -First 1
if ($potrivire) { return $potrivire.IPAddress }
}
}
catch { }
return $adrese[0].IPAddress
}
<#
.SYNOPSIS
Get the listener host address.
.DESCRIPTION
Auto-detects the host address from listener configuration.
Returns the IP address the listener is bound to.
.PARAMETER OracleHome
Oracle Home directory.
.OUTPUTS
String. The host address (IP or hostname).
.EXAMPLE
$host = Get-ListenerHost -OracleHome "C:\app\oracle\product\21c\dbhomeXE"
#>
function Get-ListenerHost {
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$OracleHome
)
$oraHome = Get-OracleHome -OracleHome $OracleHome
$lsnrctl = Join-Path $oraHome "bin\lsnrctl.exe"
if (-not (Test-Path $lsnrctl)) {
return "localhost"
}
try {
# Set Oracle environment
$env:ORACLE_HOME = $oraHome
$env:PATH = "$oraHome\bin;$env:PATH"
$output = & $lsnrctl status 2>&1 | Out-String
# Parse HOST from listener output - look for TCP endpoint (not TCPS)
# Format: (PROTOCOL=tcp)(HOST=10.0.20.130)(PORT=1521)
# Use simple IP pattern to avoid regex escaping issues
$tcpMatch = [regex]::Match($output, "PROTOCOL=tcp\).*?HOST=([0-9\.]+)")
if ($tcpMatch.Success) {
$listenerHost = $tcpMatch.Groups[1].Value
# If listener is bound to 0.0.0.0, try to get actual IP
if ($listenerHost -eq "0.0.0.0") {
$ip = Get-ServerLanIPv4
if ($ip) { return $ip }
return "localhost"
}
# If it's a specific IP (not localhost), return it
if ($listenerHost -notmatch "^127\." -and $listenerHost -ne "localhost") {
return $listenerHost
}
}
# Fallback: try any HOST= pattern
$anyHostMatch = [regex]::Match($output, "HOST=([0-9\.]+)")
if ($anyHostMatch.Success) {
$listenerHost = $anyHostMatch.Groups[1].Value
if ($listenerHost -notmatch "^127\." -and $listenerHost -ne "0.0.0.0") {
return $listenerHost
}
}
return "localhost"
}
catch {
return "localhost"
}
}
<#
.SYNOPSIS
Find Oracle Home directory.
.DESCRIPTION
Auto-detects Oracle Home from registry, environment variable, or common paths.
.PARAMETER OracleHome
Optional explicit Oracle Home path. If not specified, auto-detects.
.OUTPUTS
String. The Oracle Home path.
.EXAMPLE
$oracleHome = Get-OracleHome
#>
function Get-OracleHome {
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$OracleHome
)
# If explicitly provided, validate and return
if ($OracleHome) {
if (Test-Path -Path "$OracleHome\bin\sqlplus.exe") {
return $OracleHome
}
throw "Invalid Oracle Home: sqlplus.exe not found at $OracleHome\bin\sqlplus.exe"
}
# Try ORACLE_HOME environment variable
if ($env:ORACLE_HOME -and (Test-Path -Path "$env:ORACLE_HOME\bin\sqlplus.exe")) {
return $env:ORACLE_HOME
}
# Try registry for Oracle XE
$regPaths = @(
'HKLM:\SOFTWARE\Oracle\KEY_OraDB21Home1',
'HKLM:\SOFTWARE\Oracle\KEY_OraDB18Home1',
'HKLM:\SOFTWARE\Oracle\KEY_XE',
'HKLM:\SOFTWARE\Wow6432Node\Oracle\KEY_OraDB21Home1',
'HKLM:\SOFTWARE\Wow6432Node\Oracle\KEY_OraDB18Home1'
)
foreach ($regPath in $regPaths) {
if (Test-Path -Path $regPath) {
$oraHome = (Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue).ORACLE_HOME
if ($oraHome -and (Test-Path -Path "$oraHome\bin\sqlplus.exe")) {
return $oraHome
}
}
}
# Try common installation paths (including user-specific paths)
$currentUser = $env:USERNAME
$commonPaths = @(
"C:\app\$currentUser\product\21c\dbhomeXE",
"C:\app\$currentUser\product\21c\dbhome_1",
"C:\app\$currentUser\product\18c\dbhomeXE",
'C:\app\oracle\product\21c\dbhomeXE',
'C:\app\oracle\product\21c\dbhome_1',
'C:\app\oracle\product\18c\dbhomeXE',
'C:\app\romfast\product\21c\dbhomeXE',
'C:\app\romfast\product\21c\dbhome_1',
'C:\oraclexe\app\oracle\product\11.2.0\server',
"D:\app\$currentUser\product\21c\dbhomeXE",
'D:\app\oracle\product\21c\dbhomeXE',
'D:\app\oracle\product\18c\dbhomeXE'
)
foreach ($path in $commonPaths) {
if (Test-Path -Path "$path\bin\sqlplus.exe") {
return $path
}
}
throw "Oracle Home not found. Please specify -OracleHome parameter or set ORACLE_HOME environment variable."
}
<#
.SYNOPSIS
Get Oracle version information.
.DESCRIPTION
Detects Oracle version (XE vs SE), edition, and whether it's a Container Database (CDB).
.PARAMETER OracleHome
Oracle Home directory.
.PARAMETER ServiceName
Database service name.
.PARAMETER Username
Username for connection (default: SYSTEM).
.PARAMETER Password
Password for connection.
.OUTPUTS
PSCustomObject with Version, Edition, IsCDB, IsXE properties.
.EXAMPLE
$version = Get-OracleVersion -ServiceName "XE" -Password "oracle"
#>
function Get-OracleVersion {
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$OracleHome,
[Parameter(Mandatory = $true)]
[string]$ServiceName,
[Parameter(Mandatory = $false)]
[string]$DbHost,
[Parameter(Mandatory = $false)]
[int]$Port = 1521,
[Parameter(Mandatory = $false)]
[string]$Username = "SYSTEM",
[Parameter(Mandatory = $true)]
[string]$Password
)
$oraHome = Get-OracleHome -OracleHome $OracleHome
$sql = @"
SET PAGESIZE 0 FEEDBACK OFF VERIFY OFF HEADING OFF ECHO OFF
SELECT 'VERSION:' || version_full FROM v`$instance;
SELECT 'EDITION:' || edition FROM v`$instance;
SELECT 'CDB:' || CDB FROM v`$database;
EXIT;
"@
$result = Invoke-SqlPlus -OracleHome $oraHome -ServiceName $ServiceName `
-DbHost $DbHost -Port $Port `
-Username $Username -Password $Password -SqlCommand $sql -Silent
$versionInfo = [PSCustomObject]@{
Version = ""
Edition = ""
IsCDB = $false
IsXE = $false
FullInfo = $result
}
foreach ($line in $result -split "`n") {
if ($line -match "^VERSION:(.+)$") {
$versionInfo.Version = $Matches[1].Trim()
}
elseif ($line -match "^EDITION:(.+)$") {
$versionInfo.Edition = $Matches[1].Trim()
$versionInfo.IsXE = $versionInfo.Edition -match "XE|Express"
}
elseif ($line -match "^CDB:(.+)$") {
$versionInfo.IsCDB = $Matches[1].Trim() -eq "YES"
}
}
return $versionInfo
}
<#
.SYNOPSIS
Get the default service name based on Oracle configuration.
.DESCRIPTION
Auto-detects the appropriate service name. Returns XEPDB1 for Oracle XE CDB,
or ROA for traditional non-CDB installations.
.PARAMETER OracleHome
Oracle Home directory.
.PARAMETER SystemPassword
SYSTEM user password.
.OUTPUTS
String. The service name.
.EXAMPLE
$serviceName = Get-ServiceName -SystemPassword "oracle"
#>
function Get-ServiceName {
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$OracleHome,
[Parameter(Mandatory = $false)]
[string]$DbHost,
[Parameter(Mandatory = $false)]
[int]$Port = 1521,
[Parameter(Mandatory = $true)]
[string]$SystemPassword
)
$oraHome = Get-OracleHome -OracleHome $OracleHome
# First try to connect to XE and check if it's CDB
try {
$version = Get-OracleVersion -OracleHome $oraHome -ServiceName "XE" `
-DbHost $DbHost -Port $Port `
-Password $SystemPassword -ErrorAction Stop
if ($version.IsCDB) {
# For CDB, use XEPDB1 (the default pluggable database)
return "XEPDB1"
}
else {
return "XE"
}
}
catch {
# Try ROA as service name
try {
$null = Test-OracleConnection -OracleHome $oraHome -ServiceName "ROA" `
-DbHost $DbHost -Port $Port `
-Username "SYSTEM" -Password $SystemPassword -ErrorAction Stop
return "ROA"
}
catch {
# Default to XEPDB1 for modern XE installations
return "XEPDB1"
}
}
}
<#
.SYNOPSIS
Test Oracle database connection.
.DESCRIPTION
Attempts to connect to Oracle and verifies the connection is successful.
.PARAMETER OracleHome
Oracle Home directory.
.PARAMETER ServiceName
Database service name.
.PARAMETER Username
Username for connection.
.PARAMETER Password
Password for connection.
.OUTPUTS
Boolean. True if connection successful.
.EXAMPLE
if (Test-OracleConnection -ServiceName "XEPDB1" -Username "SYSTEM" -Password "oracle") {
Write-Host "Connected!"
}
#>
function Test-OracleConnection {
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$OracleHome,
[Parameter(Mandatory = $true)]
[string]$ServiceName,
[Parameter(Mandatory = $false)]
[string]$DbHost,
[Parameter(Mandatory = $false)]
[int]$Port = 1521,
[Parameter(Mandatory = $false)]
[string]$Username = "SYSTEM",
[Parameter(Mandatory = $true)]
[string]$Password,
[Parameter(Mandatory = $false)]
[switch]$AsSysdba
)
$oraHome = Get-OracleHome -OracleHome $OracleHome
$sql = "SELECT 'CONNECTED' FROM dual; EXIT;"
try {
$result = Invoke-SqlPlus -OracleHome $oraHome -ServiceName $ServiceName `
-DbHost $DbHost -Port $Port `
-Username $Username -Password $Password -SqlCommand $sql -AsSysdba:$AsSysdba -Silent
return $result -match "CONNECTED"
}
catch {
return $false
}
}
<#
.SYNOPSIS
Test if connected to a PDB or non-CDB.
.DESCRIPTION
Checks the current container type to determine if connected to a PDB,
the CDB root, or a non-CDB database.
.PARAMETER OracleHome
Oracle Home directory.
.PARAMETER ServiceName
Database service name.
.PARAMETER Username
Username for connection.
.PARAMETER Password
Password for connection.
.OUTPUTS
PSCustomObject with ContainerName, IsPDB, IsCDBRoot, IsNonCDB properties.
.EXAMPLE
$containerInfo = Test-PDB -ServiceName "XEPDB1" -Password "oracle"
#>
function Test-PDB {
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$OracleHome,
[Parameter(Mandatory = $true)]
[string]$ServiceName,
[Parameter(Mandatory = $false)]
[string]$DbHost,
[Parameter(Mandatory = $false)]
[int]$Port = 1521,
[Parameter(Mandatory = $false)]
[string]$Username = "SYSTEM",
[Parameter(Mandatory = $true)]
[string]$Password
)
$oraHome = Get-OracleHome -OracleHome $OracleHome
$sql = @"
SET PAGESIZE 0 FEEDBACK OFF VERIFY OFF HEADING OFF ECHO OFF
SELECT 'CONTAINER:' || SYS_CONTEXT('USERENV', 'CON_NAME') FROM dual;
SELECT 'CON_ID:' || SYS_CONTEXT('USERENV', 'CON_ID') FROM dual;
EXIT;
"@
$result = Invoke-SqlPlus -OracleHome $oraHome -ServiceName $ServiceName `
-DbHost $DbHost -Port $Port `
-Username $Username -Password $Password -SqlCommand $sql -Silent
$containerInfo = [PSCustomObject]@{
ContainerName = ""
ConId = 0
IsPDB = $false
IsCDBRoot = $false
IsNonCDB = $false
}
foreach ($line in $result -split "`n") {
if ($line -match "^CONTAINER:(.+)$") {
$containerInfo.ContainerName = $Matches[1].Trim()
}
elseif ($line -match "^CON_ID:(.+)$") {
$containerInfo.ConId = [int]$Matches[1].Trim()
}
}
if ($containerInfo.ConId -eq 0) {
$containerInfo.IsNonCDB = $true
}
elseif ($containerInfo.ConId -eq 1) {
$containerInfo.IsCDBRoot = $true
}
else {
$containerInfo.IsPDB = $true
}
return $containerInfo
}
<#
.SYNOPSIS
Execute SQL via SQL*Plus.
.DESCRIPTION
Runs a SQL command or file using SQL*Plus and returns the output.
.PARAMETER OracleHome
Oracle Home directory.
.PARAMETER ServiceName
Database service name.
.PARAMETER Username
Username for connection.
.PARAMETER Password
Password for connection.
.PARAMETER SqlCommand
SQL command(s) to execute.
.PARAMETER SqlFile
Path to SQL file to execute.
.PARAMETER AsSysdba
Connect as SYSDBA.
.PARAMETER Silent
Suppress console output.
.OUTPUTS
String. The SQL*Plus output.
.EXAMPLE
Invoke-SqlPlus -ServiceName "XEPDB1" -Username "SYSTEM" -Password "oracle" -SqlCommand "SELECT * FROM dual;"
.EXAMPLE
Invoke-SqlPlus -ServiceName "XEPDB1" -Username "SYS" -Password "oracle" -SqlFile "C:\scripts\setup.sql" -AsSysdba
#>
function Invoke-SqlPlus {
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$OracleHome,
[Parameter(Mandatory = $true)]
[string]$ServiceName,
[Parameter(Mandatory = $false)]
[string]$DbHost,
[Parameter(Mandatory = $false)]
[int]$Port = 1521,
[Parameter(Mandatory = $false)]
[string]$Username = "SYSTEM",
[Parameter(Mandatory = $true)]
[string]$Password,
[Parameter(Mandatory = $false)]
[string]$SqlCommand,
[Parameter(Mandatory = $false)]
[string]$SqlFile,
[Parameter(Mandatory = $false)]
[switch]$AsSysdba,
[Parameter(Mandatory = $false)]
[switch]$Silent
)
$oraHome = Get-OracleHome -OracleHome $OracleHome
$sqlplus = Join-Path $oraHome "bin\sqlplus.exe"
if (-not (Test-Path -Path $sqlplus)) {
throw "SQL*Plus not found at: $sqlplus"
}
# Auto-detect host from listener if not provided
if (-not $DbHost) {
$DbHost = Get-ListenerHost -OracleHome $oraHome
}
# Build connection string (always use EZConnect format for reliability)
$sysdba = if ($AsSysdba) { " as sysdba" } else { "" }
$connString = "$Username/`"$Password`"@${DbHost}:${Port}/${ServiceName}$sysdba"
$tempFile = $null
try {
if ($SqlFile) {
# Execute SQL file
if (-not (Test-Path -Path $SqlFile)) {
throw "SQL file not found: $SqlFile"
}
$arguments = "-S `"$connString`" @`"$SqlFile`""
}
else {
# Create temp file for SQL command
$tempFile = [System.IO.Path]::GetTempFileName()
$tempFile = [System.IO.Path]::ChangeExtension($tempFile, ".sql")
Set-Content -Path $tempFile -Value $SqlCommand -Encoding ASCII
$arguments = "-S `"$connString`" @`"$tempFile`""
}
# Set Oracle environment
$env:ORACLE_HOME = $oraHome
$env:PATH = "$oraHome\bin;$env:PATH"
$env:NLS_LANG = "AMERICAN_AMERICA.AL32UTF8"
# Execute SQL*Plus
$processInfo = New-Object System.Diagnostics.ProcessStartInfo
$processInfo.FileName = $sqlplus
$processInfo.Arguments = "-S `"$connString`""
$processInfo.RedirectStandardInput = $true
$processInfo.RedirectStandardOutput = $true
$processInfo.RedirectStandardError = $true
$processInfo.UseShellExecute = $false
$processInfo.CreateNoWindow = $true
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $processInfo
$process.Start() | Out-Null
# Send SQL
if ($SqlFile) {
$process.StandardInput.WriteLine("@`"$SqlFile`"")
}
else {
$process.StandardInput.WriteLine($SqlCommand)
}
$process.StandardInput.Close()
$output = $process.StandardOutput.ReadToEnd()
$errorOutput = $process.StandardError.ReadToEnd()
$process.WaitForExit()
if ($errorOutput) {
$output += "`n$errorOutput"
}
# Check for Oracle errors
if ($output -match "ORA-\d{5}:|SP2-\d{4}:") {
if (-not $Silent) {
Write-LogWarning "SQL*Plus returned errors:"
Write-Host $output -ForegroundColor Yellow
}
}
return $output
}
finally {
if ($tempFile -and (Test-Path -Path $tempFile)) {
Remove-Item -Path $tempFile -Force -ErrorAction SilentlyContinue
}
}
}
<#
.SYNOPSIS
Create Oracle directory object for Data Pump.
.DESCRIPTION
Creates a directory object in Oracle for Data Pump operations if it doesn't exist.
.PARAMETER OracleHome
Oracle Home directory.
.PARAMETER ServiceName
Database service name.
.PARAMETER Password
SYS or SYSTEM password.
.PARAMETER DirectoryName
Oracle directory object name (default: DMPDIR).
.PARAMETER DirectoryPath
File system path for the directory (default: C:\DMPDIR).
.EXAMPLE
New-OracleDirectory -ServiceName "XEPDB1" -Password "oracle" -DirectoryPath "D:\Dumps"
#>
function New-OracleDirectory {
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$OracleHome,
[Parameter(Mandatory = $true)]
[string]$ServiceName,
[Parameter(Mandatory = $true)]
[string]$Password,
[Parameter(Mandatory = $false)]
[string]$DirectoryName = "DMPDIR",
[Parameter(Mandatory = $false)]
[string]$DirectoryPath = "C:\DMPDIR"
)
# Create physical directory if it doesn't exist
if (-not (Test-Path -Path $DirectoryPath)) {
New-Item -ItemType Directory -Path $DirectoryPath -Force | Out-Null
Write-Log "Created directory: $DirectoryPath"
}
# Always drop and recreate directory to ensure correct path
# This fixes Oracle XE issue where DMPDIR may point to wrong default location
# Using DROP + CREATE instead of CREATE OR REPLACE for reliability
$sql = @"
SET PAGESIZE 0 FEEDBACK OFF VERIFY OFF HEADING OFF ECHO OFF LINESIZE 500
-- Check current directory path
VARIABLE v_path VARCHAR2(500);
VARIABLE v_exists NUMBER;
BEGIN
SELECT COUNT(*), MAX(directory_path) INTO :v_exists, :v_path
FROM dba_directories WHERE directory_name = '$DirectoryName';
END;
/
PRINT v_path
-- Drop if exists with different path
BEGIN
IF :v_exists > 0 AND UPPER(TRIM(:v_path)) != UPPER('$DirectoryPath') THEN
EXECUTE IMMEDIATE 'DROP DIRECTORY $DirectoryName';
DBMS_OUTPUT.PUT_LINE('DROPPED_DIFFERENT_PATH');
ELSIF :v_exists > 0 THEN
DBMS_OUTPUT.PUT_LINE('PATH_ALREADY_CORRECT');
END IF;
EXCEPTION
WHEN OTHERS THEN NULL;
END;
/
-- Create or replace directory
CREATE OR REPLACE DIRECTORY $DirectoryName AS '$DirectoryPath';
GRANT READ, WRITE ON DIRECTORY $DirectoryName TO PUBLIC;
-- Verify
SELECT 'VERIFIED:' || directory_path FROM dba_directories WHERE directory_name = '$DirectoryName';
EXIT;
"@
$result = Invoke-SqlPlus -OracleHome $OracleHome -ServiceName $ServiceName `
-Username "SYS" -Password $Password -SqlCommand $sql -AsSysdba -Silent
# Check verification result
if ($result -match "VERIFIED:(.+)") {
$verifiedPath = $Matches[1].Trim()
if ($verifiedPath.ToUpper() -eq $DirectoryPath.ToUpper()) {
Write-Log "Oracle directory $DirectoryName created pointing to $DirectoryPath"
# Pana aici s-au comparat doar siruri de caractere. Adevarata intrebare
# e daca procesul Oracle poate scrie efectiv acolo, si raspunsul poate fi
# nu chiar cu ACL-uri corecte pe folder: contul virtual al serviciului
# (NT SERVICE\OracleServiceXE) nu are traversare pe radacina lui C:, asa
# ca un DMPDIR pe C:\ pica abia in mijlocul importului, cu un
# ORA-29283 "nonexistent file or path" care nu arata spre cauza.
$testSql = @"
SET PAGESIZE 0 FEEDBACK OFF HEADING OFF VERIFY OFF ECHO OFF SERVEROUTPUT ON
DECLARE
f UTL_FILE.FILE_TYPE;
BEGIN
f := UTL_FILE.FOPEN('$DirectoryName', 'roa_write_test.tmp', 'w');
UTL_FILE.PUT_LINE(f, 'ok');
UTL_FILE.FCLOSE(f);
UTL_FILE.FREMOVE('$DirectoryName', 'roa_write_test.tmp');
DBMS_OUTPUT.PUT_LINE('WRITE_OK');
EXCEPTION
WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('WRITE_FAIL:' || SQLERRM);
END;
/
EXIT;
"@
$testResult = Invoke-SqlPlus -OracleHome $OracleHome -ServiceName $ServiceName `
-Username "SYS" -Password $Password -SqlCommand $testSql -AsSysdba -Silent
if ($testResult -notmatch "WRITE_OK") {
Write-LogError "Baza NU poate scrie in $DirectoryPath (directorul Oracle $DirectoryName)."
Write-LogError " $(($testResult -split "`n" | Where-Object { $_ -match 'ORA-|WRITE_FAIL' } | Select-Object -First 2) -join ' ')"
Write-LogError " Serviciul Oracle ruleaza ca NT SERVICE\OracleServiceXE si nu ajunge la calea asta"
Write-LogError " chiar daca ACL-ul pe folder pare corect - de obicei lipseste traversarea pe"
Write-LogError " radacina discului. Pune DMP-urile pe discul unde e Oracle Home si reia cu"
Write-LogError " optiunea /dmpdest:<cale> (ex. /dmpdest:D:\DMPDIR)."
return $false
}
Write-Log "Verificat: baza poate scrie in $DirectoryPath"
return $true
}
else {
Write-LogWarning "Directory path mismatch! Expected: $DirectoryPath, Got: $verifiedPath"
return $false
}
}
else {
Write-LogWarning "Could not verify directory creation"
Write-LogWarning "SQL output: $result"
return $false
}
}
<#
.SYNOPSIS
Get the default datafile path based on Oracle installation.
.DESCRIPTION
Determines the appropriate datafile location based on Oracle Home and version.
.PARAMETER OracleHome
Oracle Home directory.
.PARAMETER ServiceName
Database service name.
.PARAMETER Password
SYSTEM password.
.OUTPUTS
String. The datafile directory path.
.EXAMPLE
$datafilePath = Get-DatafilePath -ServiceName "XEPDB1" -Password "oracle"
#>
function Get-DatafilePath {
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$OracleHome,
[Parameter(Mandatory = $true)]
[string]$ServiceName,
[Parameter(Mandatory = $true)]
[string]$Password
)
$oraHome = Get-OracleHome -OracleHome $OracleHome
# Query Oracle for default datafile location
$sql = @"
SET PAGESIZE 0 FEEDBACK OFF VERIFY OFF HEADING OFF ECHO OFF LINESIZE 500
SELECT 'DATAFILE_PATH:' || SUBSTR(file_name, 1, INSTR(file_name, '\', -1))
FROM dba_data_files
WHERE tablespace_name = 'SYSTEM'
AND ROWNUM = 1;
EXIT;
"@
$result = Invoke-SqlPlus -OracleHome $oraHome -ServiceName $ServiceName `
-Username "SYSTEM" -Password $Password -SqlCommand $sql -Silent
foreach ($line in $result -split "`n") {
if ($line -match "^DATAFILE_PATH:(.+)$") {
$path = $Matches[1].Trim()
# Remove trailing backslash if present
return $path.TrimEnd('\')
}
}
# Fallback: derive path from ORACLE_HOME
# Example: C:\app\romfast\product\21c\dbhomeXE -> C:\app\romfast\product\21c\oradata\XE\XEPDB1
Write-LogWarning "Could not query datafile path from database, using fallback based on ORACLE_HOME"
$version = Get-OracleVersion -OracleHome $oraHome -ServiceName $ServiceName -Password $Password
# Try to derive oradata path from ORACLE_HOME
# Pattern: <base>\product\<version>\dbhomeXE -> <base>\product\<version>\oradata\XE\<PDB>
if ($oraHome -match "^(.+\\product\\[^\\]+)\\dbhome") {
$baseOradata = "$($Matches[1])\oradata"
if ($version.IsXE) {
if ($version.Version -match "^21") {
return "$baseOradata\XE\XEPDB1"
}
elseif ($version.Version -match "^18") {
return "$baseOradata\XE"
}
}
return $baseOradata
}
# Legacy fallback for non-standard installations
# Extract base from ORACLE_HOME (e.g., C:\app\romfast from C:\app\romfast\product\21c\dbhomeXE)
if ($oraHome -match "^([A-Z]:\\[^\\]+\\[^\\]+)\\") {
$appBase = $Matches[1]
if ($version.IsXE) {
if ($version.Version -match "^21") {
return "$appBase\oradata\XE\XEPDB1"
}
elseif ($version.Version -match "^18") {
return "$appBase\oradata\XE"
}
}
return "$appBase\oradata"
}
# Ultimate fallback - this should rarely be reached
Write-LogWarning "Could not derive datafile path from ORACLE_HOME, using hardcoded default"
return "C:\app\oracle\oradata"
}
<#
.SYNOPSIS
Execute Data Pump import.
.DESCRIPTION
Runs impdp with specified parameters.
.PARAMETER OracleHome
Oracle Home directory.
.PARAMETER ServiceName
Database service name.
.PARAMETER Username
Username for connection (default: SYSTEM).
.PARAMETER Password
Password for connection.
.PARAMETER DumpFile
Name of the dump file (in DMPDIR).
.PARAMETER LogFile
Name of the log file.
.PARAMETER Schemas
Schema(s) to import.
.PARAMETER RemapSchema
Remap schema mapping (e.g., "OLD_SCHEMA:NEW_SCHEMA").
.PARAMETER DirectoryName
Oracle directory object name (default: DMPDIR).
.PARAMETER TableExists
Table exists action: SKIP, APPEND, TRUNCATE, REPLACE (default: REPLACE).
.PARAMETER AdditionalParams
Additional impdp parameters.
.OUTPUTS
PSCustomObject with Success, LogContent, ErrorMessage properties.
.EXAMPLE
Invoke-DataPumpImport -ServiceName "XEPDB1" -Password "oracle" -DumpFile "CONTAFIN_ORACLE.dmp" -Schemas "CONTAFIN_ORACLE"
#>
function Invoke-DataPumpImport {
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$OracleHome,
[Parameter(Mandatory = $true)]
[string]$ServiceName,
[Parameter(Mandatory = $false)]
[string]$Username = "SYSTEM",
[Parameter(Mandatory = $true)]
[string]$Password,
[Parameter(Mandatory = $true)]
[string]$DumpFile,
[Parameter(Mandatory = $false)]
[string]$LogFile,
[Parameter(Mandatory = $false)]
[string[]]$Schemas,
[Parameter(Mandatory = $false)]
[string]$RemapSchema,
[Parameter(Mandatory = $false)]
[string]$DirectoryName = "DMPDIR",
[Parameter(Mandatory = $false)]
[ValidateSet('SKIP', 'APPEND', 'TRUNCATE', 'REPLACE')]
[string]$TableExists = "REPLACE",
[Parameter(Mandatory = $false)]
[string]$AdditionalParams
)
$oraHome = Get-OracleHome -OracleHome $OracleHome
$impdp = Join-Path $oraHome "bin\impdp.exe"
if (-not (Test-Path -Path $impdp)) {
throw "impdp not found at: $impdp"
}
# Build impdp command
if (-not $LogFile) {
$LogFile = [System.IO.Path]::GetFileNameWithoutExtension($DumpFile) + "_import.log"
}
$connString = "$Username/`"$Password`"@$ServiceName"
$params = @(
"`"$connString`"",
"directory=$DirectoryName",
"dumpfile=$DumpFile",
"logfile=$LogFile",
"table_exists_action=$TableExists"
)
if ($Schemas) {
$params += "schemas=$($Schemas -join ',')"
}
if ($RemapSchema) {
$params += "remap_schema=$RemapSchema"
}
if ($AdditionalParams) {
$params += $AdditionalParams
}
$arguments = $params -join " "
Write-Log "Executing: impdp $($params -join ' ' -replace $Password, '****')"
# Set Oracle environment
$env:ORACLE_HOME = $oraHome
$env:PATH = "$oraHome\bin;$env:PATH"
$env:NLS_LANG = "AMERICAN_AMERICA.AL32UTF8"
# Execute impdp
$process = Start-Process -FilePath $impdp -ArgumentList $arguments -Wait -NoNewWindow -PassThru `
-RedirectStandardOutput "$env:TEMP\impdp_out.txt" -RedirectStandardError "$env:TEMP\impdp_err.txt"
$stdout = Get-Content -Path "$env:TEMP\impdp_out.txt" -Raw -ErrorAction SilentlyContinue
$stderr = Get-Content -Path "$env:TEMP\impdp_err.txt" -Raw -ErrorAction SilentlyContinue
$result = [PSCustomObject]@{
Success = $process.ExitCode -eq 0
ExitCode = $process.ExitCode
Output = $stdout
ErrorOutput = $stderr
LogFile = $LogFile
}
# Clean up temp files
Remove-Item -Path "$env:TEMP\impdp_out.txt" -Force -ErrorAction SilentlyContinue
Remove-Item -Path "$env:TEMP\impdp_err.txt" -Force -ErrorAction SilentlyContinue
return $result
}
<#
.SYNOPSIS
Check if a user/schema exists in the database.
.DESCRIPTION
Queries DBA_USERS to check if a user exists.
.PARAMETER OracleHome
Oracle Home directory.
.PARAMETER ServiceName
Database service name.
.PARAMETER Password
SYSTEM password.
.PARAMETER SchemaName
Name of the schema/user to check.
.OUTPUTS
Boolean. True if user exists.
.EXAMPLE
if (Test-OracleUser -ServiceName "XEPDB1" -Password "oracle" -SchemaName "CONTAFIN_ORACLE") {
Write-Host "User exists"
}
#>
function Test-OracleUser {
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$OracleHome,
[Parameter(Mandatory = $true)]
[string]$ServiceName,
[Parameter(Mandatory = $true)]
[string]$Password,
[Parameter(Mandatory = $true)]
[string]$SchemaName
)
$sql = @"
SET PAGESIZE 0 FEEDBACK OFF VERIFY OFF HEADING OFF ECHO OFF
SELECT 'USER_EXISTS' FROM dba_users WHERE username = UPPER('$SchemaName');
EXIT;
"@
$result = Invoke-SqlPlus -OracleHome $OracleHome -ServiceName $ServiceName `
-Username "SYSTEM" -Password $Password -SqlCommand $sql -Silent
return $result -match "USER_EXISTS"
}
<#
.SYNOPSIS
Get object count for a schema.
.DESCRIPTION
Counts objects in a schema grouped by type.
.PARAMETER OracleHome
Oracle Home directory.
.PARAMETER ServiceName
Database service name.
.PARAMETER Password
SYSTEM password.
.PARAMETER SchemaName
Name of the schema.
.OUTPUTS
Hashtable with object types and counts.
.EXAMPLE
$counts = Get-SchemaObjectCount -ServiceName "XEPDB1" -Password "oracle" -SchemaName "CONTAFIN_ORACLE"
#>
function Get-SchemaObjectCount {
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$OracleHome,
[Parameter(Mandatory = $true)]
[string]$ServiceName,
[Parameter(Mandatory = $true)]
[string]$Password,
[Parameter(Mandatory = $true)]
[string]$SchemaName
)
$sql = @"
SET PAGESIZE 1000 FEEDBACK OFF VERIFY OFF HEADING OFF ECHO OFF LINESIZE 200
SELECT object_type || ':' || COUNT(*)
FROM dba_objects
WHERE owner = UPPER('$SchemaName')
GROUP BY object_type
ORDER BY object_type;
SELECT 'TOTAL:' || COUNT(*) FROM dba_objects WHERE owner = UPPER('$SchemaName');
SELECT 'INVALID:' || COUNT(*) FROM dba_objects WHERE owner = UPPER('$SchemaName') AND status = 'INVALID';
EXIT;
"@
$oraHome = Get-OracleHome -OracleHome $OracleHome
$result = Invoke-SqlPlus -OracleHome $oraHome -ServiceName $ServiceName `
-Username "SYSTEM" -Password $Password -SqlCommand $sql -Silent
$counts = @{}
# Parse result lines for object type counts
$lines = $result -split "`r?`n"
foreach ($line in $lines) {
$trimmed = $line.Trim()
if ($trimmed.Length -gt 0 -and $trimmed -match "^([A-Z][A-Z_ ]*):(\d+)$") {
$key = $Matches[1].Trim()
$value = [int]$Matches[2]
$counts[$key] = $value
}
}
return $counts
}
# Note: Functions are available when dot-sourced (. .\oracle-functions.ps1)
# Do NOT use Export-ModuleMember - it only works inside .psm1 modules