296 lines
9.0 KiB
PowerShell
Executable File
296 lines
9.0 KiB
PowerShell
Executable File
#!/usr/bin/env pwsh
|
|
#
|
|
# Business Central Database Export via Admin Center API
|
|
# Authenticates to Azure AD and exports BC database as BACPAC
|
|
#
|
|
|
|
param(
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$OutputPath
|
|
)
|
|
|
|
# Get configuration from environment variables
|
|
$tenantId = $env:AZURE_TENANT_ID
|
|
$clientId = $env:AZURE_CLIENT_ID
|
|
$clientSecret = $env:AZURE_CLIENT_SECRET
|
|
$environmentName = $env:BC_ENVIRONMENT_NAME
|
|
$apiVersion = $env:BC_API_VERSION
|
|
|
|
if (-not $apiVersion) {
|
|
$apiVersion = "v2.15"
|
|
}
|
|
|
|
$baseUrl = "https://api.businesscentral.dynamics.com/admin/$apiVersion"
|
|
|
|
function Write-Log {
|
|
param([string]$Message, [string]$Level = "INFO")
|
|
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
|
Write-Host "[$timestamp] [$Level] $Message"
|
|
}
|
|
|
|
function Get-AzureADToken {
|
|
param(
|
|
[string]$TenantId,
|
|
[string]$ClientId,
|
|
[string]$ClientSecret
|
|
)
|
|
|
|
Write-Log "Authenticating to Azure AD..."
|
|
|
|
$tokenUrl = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
|
|
|
|
$body = @{
|
|
client_id = $ClientId
|
|
client_secret = $ClientSecret
|
|
scope = "https://api.businesscentral.dynamics.com/.default"
|
|
grant_type = "client_credentials"
|
|
}
|
|
|
|
try {
|
|
$response = Invoke-RestMethod -Uri $tokenUrl -Method Post -Body $body -ContentType "application/x-www-form-urlencoded"
|
|
Write-Log "Successfully authenticated to Azure AD"
|
|
return $response.access_token
|
|
}
|
|
catch {
|
|
Write-Log "Failed to authenticate: $_" "ERROR"
|
|
throw
|
|
}
|
|
}
|
|
|
|
function Start-DatabaseExport {
|
|
param(
|
|
[string]$Token,
|
|
[string]$EnvironmentName
|
|
)
|
|
|
|
Write-Log "Initiating database export for environment: $EnvironmentName"
|
|
|
|
$headers = @{
|
|
"Authorization" = "Bearer $Token"
|
|
"Content-Type" = "application/json"
|
|
}
|
|
|
|
$exportUrl = "$baseUrl/applications/businesscentral/environments/$EnvironmentName/databaseExports"
|
|
|
|
try {
|
|
$response = Invoke-RestMethod -Uri $exportUrl -Method Post -Headers $headers
|
|
Write-Log "Database export initiated successfully"
|
|
return $response
|
|
}
|
|
catch {
|
|
Write-Log "Failed to initiate export: $_" "ERROR"
|
|
Write-Log "Response: $($_.ErrorDetails.Message)" "ERROR"
|
|
throw
|
|
}
|
|
}
|
|
|
|
function Get-ExportStatus {
|
|
param(
|
|
[string]$Token,
|
|
[string]$EnvironmentName
|
|
)
|
|
|
|
$headers = @{
|
|
"Authorization" = "Bearer $Token"
|
|
}
|
|
|
|
$statusUrl = "$baseUrl/applications/businesscentral/environments/$EnvironmentName/databaseExports"
|
|
|
|
try {
|
|
$response = Invoke-RestMethod -Uri $statusUrl -Method Get -Headers $headers
|
|
return $response
|
|
}
|
|
catch {
|
|
Write-Log "Failed to get export status: $_" "ERROR"
|
|
return $null
|
|
}
|
|
}
|
|
|
|
function Wait-ForExport {
|
|
param(
|
|
[string]$Token,
|
|
[string]$EnvironmentName,
|
|
[int]$MaxWaitMinutes = 120
|
|
)
|
|
|
|
Write-Log "Waiting for export to complete (max $MaxWaitMinutes minutes)..."
|
|
|
|
$startTime = Get-Date
|
|
$pollInterval = 30 # seconds
|
|
|
|
while ($true) {
|
|
$elapsed = ((Get-Date) - $startTime).TotalMinutes
|
|
|
|
if ($elapsed -gt $MaxWaitMinutes) {
|
|
Write-Log "Export timeout exceeded ($MaxWaitMinutes minutes)" "ERROR"
|
|
return $null
|
|
}
|
|
|
|
$status = Get-ExportStatus -Token $Token -EnvironmentName $EnvironmentName
|
|
|
|
if ($null -eq $status -or $status.value.Count -eq 0) {
|
|
Write-Log "No export found, waiting..." "WARN"
|
|
Start-Sleep -Seconds $pollInterval
|
|
continue
|
|
}
|
|
|
|
# Get the most recent export
|
|
$latestExport = $status.value | Sort-Object -Property createdOn -Descending | Select-Object -First 1
|
|
|
|
$exportStatus = $latestExport.status
|
|
|
|
Write-Log "Export status: $exportStatus (Elapsed: $([math]::Round($elapsed, 1)) min)"
|
|
|
|
switch ($exportStatus) {
|
|
"complete" {
|
|
Write-Log "Export completed successfully"
|
|
return $latestExport
|
|
}
|
|
"failed" {
|
|
Write-Log "Export failed" "ERROR"
|
|
if ($latestExport.failureReason) {
|
|
Write-Log "Failure reason: $($latestExport.failureReason)" "ERROR"
|
|
}
|
|
return $null
|
|
}
|
|
"inProgress" {
|
|
Write-Log "Export in progress..."
|
|
}
|
|
"queued" {
|
|
Write-Log "Export queued..."
|
|
}
|
|
default {
|
|
Write-Log "Unknown status: $exportStatus" "WARN"
|
|
}
|
|
}
|
|
|
|
Start-Sleep -Seconds $pollInterval
|
|
}
|
|
}
|
|
|
|
function Download-Export {
|
|
param(
|
|
[object]$Export,
|
|
[string]$OutputPath
|
|
)
|
|
|
|
if (-not $Export.blobUri) {
|
|
Write-Log "No download URI available in export object" "ERROR"
|
|
return $false
|
|
}
|
|
|
|
$downloadUri = $Export.blobUri
|
|
|
|
Write-Log "Downloading BACPAC from: $downloadUri"
|
|
Write-Log "Saving to: $OutputPath"
|
|
|
|
try {
|
|
# Use Invoke-WebRequest for better progress tracking
|
|
$ProgressPreference = 'SilentlyContinue' # Disable progress bar for better performance
|
|
Invoke-WebRequest -Uri $downloadUri -OutFile $OutputPath -UseBasicParsing
|
|
|
|
if (Test-Path $OutputPath) {
|
|
$fileSize = (Get-Item $OutputPath).Length
|
|
$fileSizeMB = [math]::Round($fileSize / 1MB, 2)
|
|
Write-Log "Download completed successfully ($fileSizeMB MB)"
|
|
return $true
|
|
}
|
|
else {
|
|
Write-Log "Download failed - file not found" "ERROR"
|
|
return $false
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log "Download failed: $_" "ERROR"
|
|
return $false
|
|
}
|
|
}
|
|
|
|
# Main execution
|
|
try {
|
|
Write-Log "========================================="
|
|
Write-Log "BC Database Export Script"
|
|
Write-Log "========================================="
|
|
Write-Log "Environment: $environmentName"
|
|
Write-Log "API Version: $apiVersion"
|
|
Write-Log "Output Path: $OutputPath"
|
|
|
|
# Step 1: Get Azure AD token
|
|
$token = Get-AzureADToken -TenantId $tenantId -ClientId $clientId -ClientSecret $clientSecret
|
|
|
|
# Step 2: Check for existing in-progress exports
|
|
Write-Log "Checking for existing exports..."
|
|
$existingStatus = Get-ExportStatus -Token $token -EnvironmentName $environmentName
|
|
|
|
$activeExport = $null
|
|
if ($existingStatus -and $existingStatus.value.Count -gt 0) {
|
|
$latestExport = $existingStatus.value | Sort-Object -Property createdOn -Descending | Select-Object -First 1
|
|
|
|
if ($latestExport.status -eq "inProgress" -or $latestExport.status -eq "queued") {
|
|
Write-Log "Found existing export in progress (created: $($latestExport.createdOn))"
|
|
$activeExport = $latestExport
|
|
|
|
# Ask if we should wait for it or start a new one
|
|
$created = [DateTime]::Parse($latestExport.createdOn)
|
|
$age = (Get-Date) - $created
|
|
|
|
if ($age.TotalHours -lt 2) {
|
|
Write-Log "Existing export is recent (age: $([math]::Round($age.TotalMinutes, 1)) min), will wait for it"
|
|
}
|
|
else {
|
|
Write-Log "Existing export is old (age: $([math]::Round($age.TotalHours, 1)) hours), starting new export" "WARN"
|
|
$activeExport = Start-DatabaseExport -Token $token -EnvironmentName $environmentName
|
|
}
|
|
}
|
|
elseif ($latestExport.status -eq "complete") {
|
|
$created = [DateTime]::Parse($latestExport.createdOn)
|
|
$age = (Get-Date) - $created
|
|
|
|
if ($age.TotalHours -lt 1) {
|
|
Write-Log "Found recent completed export (age: $([math]::Round($age.TotalMinutes, 1)) min)"
|
|
Write-Log "Using existing export to avoid API limits"
|
|
$activeExport = $latestExport
|
|
}
|
|
else {
|
|
Write-Log "Latest export is too old, starting new export"
|
|
$activeExport = Start-DatabaseExport -Token $token -EnvironmentName $environmentName
|
|
}
|
|
}
|
|
}
|
|
|
|
# Step 3: Start new export if needed
|
|
if (-not $activeExport) {
|
|
$activeExport = Start-DatabaseExport -Token $token -EnvironmentName $environmentName
|
|
}
|
|
|
|
# Step 4: Wait for export to complete (if not already complete)
|
|
if ($activeExport.status -ne "complete") {
|
|
$completedExport = Wait-ForExport -Token $token -EnvironmentName $environmentName -MaxWaitMinutes 120
|
|
|
|
if (-not $completedExport) {
|
|
Write-Log "Export did not complete successfully" "ERROR"
|
|
exit 1
|
|
}
|
|
|
|
$activeExport = $completedExport
|
|
}
|
|
|
|
# Step 5: Download the BACPAC file
|
|
$downloadSuccess = Download-Export -Export $activeExport -OutputPath $OutputPath
|
|
|
|
if (-not $downloadSuccess) {
|
|
Write-Log "Failed to download export" "ERROR"
|
|
exit 1
|
|
}
|
|
|
|
Write-Log "========================================="
|
|
Write-Log "Export completed successfully"
|
|
Write-Log "========================================="
|
|
exit 0
|
|
}
|
|
catch {
|
|
Write-Log "Unexpected error: $_" "ERROR"
|
|
Write-Log "Stack trace: $($_.ScriptStackTrace)" "ERROR"
|
|
exit 1
|
|
}
|