OCI Windows Server Disk Space Alert – Setup Guide

OCI Windows Server Disk Space Alert - Setup Guide

Proactively monitoring server storage is a critical task for any system administrator. When running Windows Server instances within Oracle Cloud Infrastructure (OCI), unexpected disk utilization can lead to application downtime and database corruption. This step-by-step guide explains how to deploy and configure the Send-DiskSpaceAlert.ps1 PowerShell script on a Windows Server hosted in OCI. The script monitors free disk space and sends polished HTML email alerts using the OCI Email Delivery SMTP service when drives fall below configured thresholds.


Prerequisites

  • Windows Server 2016 / 2019 / 2022 / 2025 hosted in OCI
  • PowerShell 5.1 or later (included in all supported Windows Server versions)
  • An active OCI Email Delivery configuration (see Configuring Oracle Cloud Email Delivery Service)
  • Administrator access on the server

Step 1 — Copy the Script

Create a folder for the script and place Send-DiskSpaceAlert.ps1 inside it:

C:\script\Send-DiskSpaceAlert.ps1

The script will also write two files to this same folder automatically:

  • DiskAlertLog.txt — rolling log of every check and alert
  • smtp_cred.xml — encrypted SMTP credential (created in Step 3)

Step 2 — Edit the CONFIG Block

Open the script in a text editor and update the $config section near the top with your OCI Email Delivery details:

# =============================================================================
# Send-DiskSpaceAlert.ps1
# Monitors disk free space and sends email alert via OCI Email Delivery
#
# SETUP:
#    1. Edit the CONFIG section below
#    2. Save SMTP password: .\Send-DiskSpaceAlert.ps1 -SaveCredential
#    3. Test email:          .\Send-DiskSpaceAlert.ps1 -Test
#    4. Schedule it:         .\Send-DiskSpaceAlert.ps1 -CreateScheduledTask
# =============================================================================
[CmdletBinding()]
param(
    [switch]$SaveCredential,
    [switch]$Test,
    [switch]$CreateScheduledTask
)
# =============================================================================
# CONFIG - Edit these values
# =============================================================================
$config = @{
    WarningThresholdGB   = 20
    CriticalThresholdGB  = 10
    DrivesToMonitor      = @()   # Empty = all drives. Or specify e.g. @("C","D")
    SmtpServer           = "smtp.email.us-ashburn-1.oci.oraclecloud.com" # Replace with your region endpoint
    SmtpPort             = 587
    UseSsl               = $true
    FromAddress          = "alerts@yourdomain.com"                       # Must match an OCI Approved Sender
    ToAddresses          = @("admin@yourdomain.com")
    EmailSubjectPrefix   = "[OCI-DiskAlert]"
    SmtpUsername         = "ocid1.user.oc1..your-smtp-user@yourtenant.com"
    CredentialFile       = "$PSScriptRoot\smtp_cred.xml"
    LogFile              = "$PSScriptRoot\DiskAlertLog.txt"
    MaxLogSizeMB         = 5
    TaskName             = "DiskSpaceAlert"
    TaskIntervalMinutes  = 30
}
# =============================================================================
function Write-Log {
    param([string]$Message, [string]$Level = "INFO")
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $line = "[$timestamp] [$Level] $Message"
    Write-Host $line
    Add-Content -Path $config.LogFile -Value $line -ErrorAction SilentlyContinue
    if (Test-Path $config.LogFile) {
        $logSizeMB = (Get-Item $config.LogFile).Length / 1MB
        if ($logSizeMB -gt $config.MaxLogSizeMB) {
            $lines   = Get-Content $config.LogFile
            $trimmed = $lines | Select-Object -Last 500
            $trimmed | Set-Content $config.LogFile
            Write-Log "Log trimmed to last 500 lines"
        }
    }
}
function Save-SmtpCredential {
    Write-Host "Enter OCI SMTP credentials to save to: $($config.CredentialFile)"
    $cred = Get-Credential -UserName $config.SmtpUsername -Message "Enter OCI SMTP password"
    $cred | Export-Clixml -Path $config.CredentialFile
    Write-Host "Credential saved." -ForegroundColor Green
    Write-Host "NOTE: Encrypted to the current Windows user/machine only." -ForegroundColor Yellow
}
function Get-SmtpCredential {
    if ([string]::IsNullOrWhiteSpace($config.SmtpUsername)) { return $null }
    if (-not (Test-Path $config.CredentialFile)) {
        Write-Log "Credential file not found: $($config.CredentialFile). Run with -SaveCredential first." "ERROR"
        return $null
    }
    try {
        return Import-Clixml -Path $config.CredentialFile
    } catch {
        Write-Log "Failed to load credential: $_" "ERROR"
        return $null
    }
}
function Send-AlertEmail {
    param(
        [string]$Subject,
        [string]$Body,
        [string]$Priority = "Normal"
    )
    $mailParams = @{
        SmtpServer  = $config.SmtpServer
        Port        = $config.SmtpPort
        UseSsl      = $config.UseSsl
        From        = $config.FromAddress
        To          = $config.ToAddresses
        Subject     = "$($config.EmailSubjectPrefix) $Subject"
        Body        = $Body
        BodyAsHtml  = $true
        Priority    = $Priority
        ErrorAction = "Stop"
    }
    $cred = Get-SmtpCredential
    if ($cred) { $mailParams.Credential = $cred }
    try {
        Send-MailMessage @mailParams
        Write-Log "Email sent: $Subject"
        return $true
    } catch {
        Write-Log "Failed to send email: $_" "ERROR"
        return $false
    }
}
function Get-DiskInfo {
    $drives = Get-PSDrive -PSProvider FileSystem | Where-Object {
        $_.Used -ne $null -and $_.Free -ne $null -and ($_.Used + $_.Free) -gt 0
    }
    if ($config.DrivesToMonitor.Count -gt 0) {
        $drives = $drives | Where-Object { $config.DrivesToMonitor -contains $_.Name }
    }
    return $drives | ForEach-Object {
        $totalGB = [math]::Round(($_.Used + $_.Free) / 1GB, 2)
        $freeGB  = [math]::Round($_.Free / 1GB, 2)
        $usedGB  = [math]::Round($_.Used / 1GB, 2)
        $freePct = if ($totalGB -gt 0) { [math]::Round(($freeGB / $totalGB) * 100, 1) } else { 0 }
        [PSCustomObject]@{
            Drive       = $_.Name
            TotalGB     = $totalGB
            FreeGB      = $freeGB
            UsedGB      = $usedGB
            FreePercent = $freePct
        }
    }
}
function Build-EmailBody {
    param(
        [array]$AlertDrives,
        [array]$AllDrives,
        [string]$AlertLevel
    )
    $headerColor = if ($AlertLevel -eq "CRITICAL") { "#c0392b" } else { "#e67e22" }
    $hostname    = $env:COMPUTERNAME
    $timestamp   = Get-Date -Format "dddd, MMMM dd yyyy HH:mm:ss"
    $warnGB      = $config.WarningThresholdGB
    $critGB      = $config.CriticalThresholdGB
    # Build alert list items using string concatenation (avoids PowerShell parsing HTML)
    $alertItems = ""
    foreach ($d in $AlertDrives) {
        $alertItems += '<li>Drive <strong>' + $d.Drive + ':\</strong> - <strong>' + $d.FreeGB + ' GB</strong> free (' + $d.FreePercent + '%)</li>'
    }
    # Build drive table rows
    $driveRows = ""
    foreach ($d in $AllDrives) {
        $usedPct = [math]::Round(($d.UsedGB / $d.TotalGB) * 100, 1)
        $freeColor = if ($d.FreeGB -lt $critGB)     { "#c0392b" }
                     elseif ($d.FreeGB -lt $warnGB) { "#e67e22" }
                     else                           { "#27ae60" }
        $barWidth = [math]::Min($usedPct, 100)
        $bar  = '<div style="background:#e0e0e0;border-radius:4px;height:10px;width:200px;display:inline-block;vertical-align:middle;">'
        $bar += '<div style="background:' + $freeColor + ';width:' + $barWidth + '%;height:10px;border-radius:4px;"></div></div>'
        $driveRows += '<tr style="border-bottom:1px solid #eee;">'
        $driveRows += '<td style="padding:8px 12px;font-weight:bold;">'  + $d.Drive + ':\</td>'
        $driveRows += '<td style="padding:8px 12px;">'                    + $d.TotalGB + ' GB</td>'
        $driveRows += '<td style="padding:8px 12px;color:' + $freeColor + ';font-weight:bold;">' + $d.FreeGB + ' GB</td>'
        $driveRows += '<td style="padding:8px 12px;">'                    + $usedPct + '%</td>'
        $driveRows += '<td style="padding:8px 12px;">'                    + $bar + '</td>'
        $driveRows += '</tr>'
    }
    # Assemble full HTML body
    $html  = '<!DOCTYPE html><html><body style="font-family:Segoe UI,Arial,sans-serif;background:#f5f5f5;margin:0;padding:20px;">'
    $html += '<div style="max-width:620px;margin:0 auto;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.1);">'
    # Header banner
    $html += '<div style="background:' + $headerColor + ';padding:20px 24px;">'
    $html += '<h2 style="color:#fff;margin:0;font-size:20px;">[!] Disk Space ' + $AlertLevel + '</h2>'
    $html += '<p style="color:rgba(255,255,255,0.85);margin:4px 0 0;font-size:13px;">' + $hostname + ' - ' + $timestamp + '</p>'
    $html += '</div>'
    # Alert summary
    $html += '<div style="padding:20px 24px;border-bottom:1px solid #eee;">'
    $html += '<p style="margin:0 0 8px;font-size:14px;color:#333;">The following drive(s) have fallen below the threshold:</p>'
    $html += '<ul style="margin:0;padding-left:20px;font-size:14px;color:#555;">' + $alertItems + '</ul>'
    $html += '<p style="margin:12px 0 0;font-size:13px;color:#888;">Thresholds - Warning: <strong>' + $warnGB + ' GB</strong> | Critical: <strong>' + $critGB + ' GB</strong></p>'
    $html += '</div>'
    # All drives table
    $html += '<div style="padding:20px 24px;">'
    $html += '<p style="margin:0 0 12px;font-size:14px;font-weight:bold;color:#333;">All Monitored Drives</p>'
    $html += '<table style="width:100%;border-collapse:collapse;font-size:13px;color:#444;">'
    $html += '<thead><tr style="background:#f8f8f8;text-align:left;">'
    $html += '<th style="padding:8px 12px;">Drive</th>'
    $html += '<th style="padding:8px 12px;">Total</th>'
    $html += '<th style="padding:8px 12px;">Free</th>'
    $html += '<th style="padding:8px 12px;">Used %</th>'
    $html += '<th style="padding:8px 12px;">Usage</th>'
    $html += '</tr></thead>'
    $html += '<tbody>' + $driveRows + '</tbody></table></div>'
    # Footer
    $html += '<div style="padding:12px 24px;background:#f8f8f8;border-top:1px solid #eee;">'
    $html += '<p style="margin:0;font-size:11px;color:#aaa;">Sent via OCI Email Delivery by Send-DiskSpaceAlert.ps1 on ' + $hostname + '</p>'
    $html += '</div></div></body></html>'
    return $html
}
function Register-AlertTask {
    $scriptPath = $PSCommandPath
    $argString  = '-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + $scriptPath + '"'
    $action     = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $argString
    $trigger    = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Minutes $config.TaskIntervalMinutes) -Once -At (Get-Date)
    $principal  = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
    $settings   = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 5) -RestartCount 2 -RestartInterval (New-TimeSpan -Minutes 1)
    try {
        Register-ScheduledTask -TaskName $config.TaskName `
            -Action $action -Trigger $trigger -Principal $principal -Settings $settings `
            -Description "Monitors disk free space and sends email alerts via OCI Email Delivery" -Force | Out-Null
        Write-Host "Scheduled task '$($config.TaskName)' created - runs every $($config.TaskIntervalMinutes) min as SYSTEM." -ForegroundColor Green
        Write-Host "Remember: save credentials via PsExec as SYSTEM so the task can decrypt the password." -ForegroundColor Yellow
    } catch {
        Write-Log "Failed to create scheduled task: $_" "ERROR"
    }
}
# =============================================================================
# MAIN
# =============================================================================
if ($SaveCredential)      { Save-SmtpCredential; exit 0 }
if ($CreateScheduledTask) { Register-AlertTask;  exit 0 }
Write-Log "Starting disk space check on $env:COMPUTERNAME"
$allDrives      = Get-DiskInfo
$criticalDrives = $allDrives | Where-Object { $_.FreeGB -lt $config.CriticalThresholdGB }
$warningDrives  = $allDrives | Where-Object { $_.FreeGB -lt $config.WarningThresholdGB -and $_.FreeGB -ge $config.CriticalThresholdGB }
foreach ($d in $allDrives) {
    Write-Log "Drive $($d.Drive):\ - Free: $($d.FreeGB) GB / $($d.TotalGB) GB ($($d.FreePercent)% free)"
}
if ($Test) {
    Write-Log "TEST MODE - sending test email via OCI Email Delivery"
    $body = Build-EmailBody -AlertDrives $allDrives -AllDrives $allDrives -AlertLevel "WARNING"
    Send-AlertEmail -Subject "TEST - Disk Space Check on $env:COMPUTERNAME" -Body $body
    exit 0
}
if ($criticalDrives) {
    Write-Log "CRITICAL: $($criticalDrives.Count) drive(s) below $($config.CriticalThresholdGB) GB" "ERROR"
    $body = Build-EmailBody -AlertDrives $criticalDrives -AllDrives $allDrives -AlertLevel "CRITICAL"
    Send-AlertEmail -Subject "CRITICAL - Low Disk Space on $env:COMPUTERNAME" -Body $body -Priority "High"
}
elseif ($warningDrives) {
    Write-Log "WARNING: $($warningDrives.Count) drive(s) below $($config.WarningThresholdGB) GB" "WARN"
    $body = Build-EmailBody -AlertDrives $warningDrives -AllDrives $allDrives -AlertLevel "WARNING"
    Send-AlertEmail -Subject "WARNING - Low Disk Space on $env:COMPUTERNAME" -Body $body
}
else {
    Write-Log "All drives OK. No alert sent."
}
Write-Log "Disk space check complete."

Step 3 — Save the SMTP Credential Securely

The script stores the OCI SMTP password encrypted on disk using Windows DPAPI (Data Protection API) via Export-Clixml. The encrypted file can only be decrypted by the same Windows user account that saved it.

Why this matters

The scheduled task (Step 5) runs as the SYSTEM account. If you save the credential as your own user, SYSTEM cannot decrypt it and the task will fail silently. You must save the credential while running as SYSTEM.

How to save the credential as SYSTEM using PsExec

1. Download PsExec (part of Microsoft Sysinternals):

Invoke-WebRequest -Uri "https://live.sysinternals.com/PsExec64.exe" -OutFile "C:\script\PsExec64.exe"

2. Open a SYSTEM-context PowerShell window (run this from an elevated prompt):

C:\script\PsExec64.exe -s -i powershell.exe

A new PowerShell window opens. Verify it is running as SYSTEM:

whoami
# Expected output: nt authority\system

3. Run the save command from that SYSTEM PowerShell window:

C:\script\Send-DiskSpaceAlert.ps1 -SaveCredential

Enter your OCI SMTP username and generated password when prompted. The credential is saved to smtp_cred.xml encrypted under the SYSTEM account.

4. Close the PsExec window.


Step 4 — Send a Test Email

Before scheduling, verify the script works end-to-end:

# Run from a normal elevated PowerShell
C:\script\Send-DiskSpaceAlert.ps1 -Test

This sends a test email immediately regardless of actual disk usage. Check your inbox and confirm the HTML table renders correctly with drive information.

If the email does not arrive, check C:\script\DiskAlertLog.txt for error details (such as OCI egress security list blocks or unapproved sender errors).


Step 5 — Create the Scheduled Task

Register the script as a Windows Scheduled Task that runs every 30 minutes as SYSTEM:

# Must be run from an elevated (Administrator) PowerShell
C:\script\Send-DiskSpaceAlert.ps1 -CreateScheduledTask

To verify it was created:

  1. Open Task Scheduler (taskschd.msc)
  2. Look for DiskSpaceAlert under Task Scheduler Library
  3. Right-click → Run to trigger it immediately
  4. Check DiskAlertLog.txt to confirm it executed successfully as SYSTEM

Alert Behaviour

The script sends two levels of alert:

Level Condition Email Priority
WARNING Free space below WarningThresholdGB (default 20 GB) Normal
CRITICAL Free space below CriticalThresholdGB (default 10 GB) High

Each email includes a colour-coded HTML table showing all monitored drives with their total size, free space, used percentage, and a visual usage bar.

No email is sent if all drives are above the warning threshold — the result is only written to the log file.


Changing the Check Interval

The default interval is every 30 minutes. To change it, edit TaskIntervalMinutes in the CONFIG block before running -CreateScheduledTask:

TaskIntervalMinutes = 15   # Check every 15 minutes

If the task already exists, delete it from Task Scheduler and re-run -CreateScheduledTask.


Log File

The script maintains a rolling log at C:\script\DiskAlertLog.txt. It is automatically trimmed to the last 500 lines when it exceeds 5 MB (configurable via MaxLogSizeMB).

Example log output:

[2026-05-12 08:30:01] [INFO]  Starting disk space check on SERVER01
[2026-05-12 08:30:01] [INFO]  Drive C:\ - Free: 8.42 GB / 127.00 GB (6.6% free)
[2026-05-12 08:30:01] [ERROR] CRITICAL: 1 drive(s) below 10 GB
[2026-05-12 08:30:03] [INFO]  Email sent: CRITICAL - Low Disk Space on SERVER01

Leave a Reply

Your email address will not be published. Required fields are marked *