Hands-on LabLesson 13 of 16

Lab: System Administration Scripts

Build four production-grade sysadmin scripts: a disk usage reporter, a service health checker, a log file cleanup tool, and a scheduled maintenance task registration script.

These four labs cover the core patterns used by platform and operations engineers every day. Each exercise is a complete, functional script you can adapt to real environments. They collectively exercise cmdlets from file system management, process/service management, error handling, objects/JSON, and scheduling.

🧪 Lab 1 — Disk Usage Report

Build a script that surveys all fixed drives on a machine, calculates used and free space, flags drives exceeding a configurable warning threshold, and exports results to a CSV report.

powershell
#Requires -Version 5.1
<#
.SYNOPSIS
    Generates a disk usage report and flags drives over a threshold.
.PARAMETER WarningThresholdPct
    Alert when a drive is more than this percentage full. Default: 80.
.PARAMETER OutputPath
    CSV file output path. Default: .\disk-report.csv
.EXAMPLE
    .\Get-DiskReport.ps1 -WarningThresholdPct 75 -OutputPath C:\Reports\disk.csv
#>
[CmdletBinding()]
param(
    [ValidateRange(1, 99)]
    [int]$WarningThresholdPct = 80,

    [string]$OutputPath = ".\disk-report.csv"
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$report = Get-PSDrive -PSProvider FileSystem | ForEach-Object {
    $totalBytes = $_.Used + $_.Free
    $usedPct = if ($totalBytes -gt 0) {
        [math]::Round($_.Used / $totalBytes * 100, 1)
    } else { 0 }

    [PSCustomObject]@{
        Drive          = $_.Name
        RootPath       = $_.Root
        TotalGB        = [math]::Round($totalBytes / 1GB, 2)
        UsedGB         = [math]::Round($_.Used / 1GB, 2)
        FreeGB         = [math]::Round($_.Free / 1GB, 2)
        UsedPct        = $usedPct
        Status         = if ($usedPct -gt $WarningThresholdPct) { "WARNING" } else { "OK" }
        ReportedAt     = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
    }
}

# Output to console
$report | Format-Table -AutoSize

# Flag warnings
$warnings = $report | Where-Object Status -eq "WARNING"
if ($warnings) {
    Write-Warning "The following drives exceed ${WarningThresholdPct}% usage:"
    $warnings | ForEach-Object { Write-Warning "  Drive $($_.Drive): $($_.UsedPct)% used ($($_.FreeGB) GB free)" }
}

# Export CSV
$report | Export-Csv -Path $OutputPath -NoTypeInformation
Write-Host "Report saved to $OutputPath"
💡
Extending This Script

Add -ComputerName support using Invoke-Command (Lesson 10) to query multiple servers at once. Combine with Send-MailMessage or an Azure Logic App webhook to email the report when warnings are detected. This is the standard SRE disk monitoring pattern before a proper monitoring stack is in place.

🧪 Lab 2 — Service Health Checker

Build a script that checks the status of a configurable list of required services, reports their states as structured objects, and exits with a non-zero code if any required service is not running (making it usable as a pipeline check gate).

powershell
#Requires -Version 5.1
<#
.SYNOPSIS
    Checks health of a list of Windows services.
.PARAMETER Services
    List of service names to check. Defaults to common IIS services.
.PARAMETER ExportJson
    Path to export the health report as JSON.
.EXAMPLE
    .\Test-ServiceHealth.ps1 -Services @("W3SVC","WAS","Spooler") -ExportJson .\health.json
#>
[CmdletBinding()]
param(
    [string[]]$Services = @("W3SVC", "WAS", "wudfsvc"),
    [string]$ExportJson = ""
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$healthReport = @()

foreach ($serviceName in $Services) {
    try {
        $svc = Get-Service -Name $serviceName -ErrorAction Stop
        $healthReport += [PSCustomObject]@{
            ServiceName  = $svc.Name
            DisplayName  = $svc.DisplayName
            Status       = $svc.Status.ToString()
            StartType    = $svc.StartType.ToString()
            IsRunning    = $svc.Status -eq 'Running'
            CheckedAt    = (Get-Date -Format "o")
        }
    }
    catch {
        Write-Warning "Service '$serviceName' not found: $_"
        $healthReport += [PSCustomObject]@{
            ServiceName  = $serviceName
            DisplayName  = "UNKNOWN"
            Status       = "NotFound"
            StartType    = "N/A"
            IsRunning    = $false
            CheckedAt    = (Get-Date -Format "o")
        }
    }
}

$healthReport | Format-Table ServiceName, Status, IsRunning, StartType -AutoSize

if ($ExportJson) {
    $healthReport | ConvertTo-Json -Depth 5 | Set-Content -Path $ExportJson -Encoding UTF8
    Write-Host "Health report saved to $ExportJson"
}

# Pipeline gate: fail if any required service is not running
$notRunning = $healthReport | Where-Object IsRunning -eq $false
if ($notRunning) {
    $notRunning | ForEach-Object { Write-Error "Required service NOT running: $($_.ServiceName) (Status: $($_.Status))" }
    exit 1
}

Write-Host "All $($Services.Count) required services are running."

🧪 Lab 3 — Log File Cleanup

Build a script that scans a log directory for files older than a configurable age, lists what would be deleted (using -WhatIf mode first), then removes them and reports disk space recovered.

powershell
#Requires -Version 5.1
<#
.SYNOPSIS
    Cleans up log files older than N days from a target directory.
.PARAMETER LogDirectory
    Directory to clean. Defaults to C:\Logs.
.PARAMETER RetentionDays
    Files older than this many days are deleted. Default: 30.
.PARAMETER WhatIf
    If specified, shows what would be deleted without deleting.
.EXAMPLE
    .\Clear-OldLogs.ps1 -LogDirectory "D:\AppLogs" -RetentionDays 14
    .\Clear-OldLogs.ps1 -LogDirectory "D:\AppLogs" -WhatIf
#>
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param(
    [string]$LogDirectory = "C:\Logs",
    [ValidateRange(1, 365)]
    [int]$RetentionDays = 30
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

if (-not (Test-Path -Path $LogDirectory)) {
    Write-Error "Directory does not exist: $LogDirectory"
    exit 1
}

$cutoffDate = (Get-Date).AddDays(-$RetentionDays)
Write-Verbose "Cutoff: files older than $($cutoffDate.ToString('yyyy-MM-dd'))"

$candidates = Get-ChildItem -Path $LogDirectory -Recurse -File |
    Where-Object LastWriteTime -lt $cutoffDate

if ($candidates.Count -eq 0) {
    Write-Host "No files older than $RetentionDays days found in $LogDirectory."
    exit 0
}

$totalSize = ($candidates | Measure-Object -Property Length -Sum).Sum
$totalSizeMB = [math]::Round($totalSize / 1MB, 2)
Write-Host "Found $($candidates.Count) files totalling ${totalSizeMB} MB for deletion."

$deletedCount = 0
$deletedBytes = 0

foreach ($file in $candidates) {
    if ($PSCmdlet.ShouldProcess($file.FullName, "Delete")) {
        try {
            $deletedBytes += $file.Length
            Remove-Item -Path $file.FullName -Force
            $deletedCount++
            Write-Verbose "Deleted: $($file.FullName)"
        }
        catch {
            Write-Warning "Failed to delete $($file.FullName): $_"
        }
    }
}

$recoveredMB = [math]::Round($deletedBytes / 1MB, 2)
Write-Host "Deleted $deletedCount files, recovered ${recoveredMB} MB."
🔒
Always Test Log Cleanup with -WhatIf First

Run the cleanup script with -WhatIf at least once in any new environment to confirm the file selection logic matches your expectations before files are irreversibly deleted. Set RetentionDays conservatively (90+ days) on first run until you understand the log volume patterns.

🧪 Lab 4 — Scheduled Task Registration

Register the disk report and log cleanup scripts as Windows Scheduled Tasks that run automatically without user interaction.

powershell
#Requires -RunAsAdministrator
<#
.SYNOPSIS
    Registers maintenance scripts as Windows Scheduled Tasks.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    [string]$ScriptsFolder = "C:\Scripts\Maintenance"
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

function Register-MaintenanceTask {
    param(
        [string]$TaskName,
        [string]$ScriptPath,
        [string]$Arguments,
        [string]$ScheduleDescription,
        $Trigger
    )

    if ($PSCmdlet.ShouldProcess($TaskName, "Register Scheduled Task")) {
        # Remove if already exists
        if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) {
            Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
        }

        $action = New-ScheduledTaskAction `
            -Execute "pwsh.exe" `
            -Argument "-NonInteractive -ExecutionPolicy Bypass -File `"$ScriptPath`" $Arguments"

        $settings = New-ScheduledTaskSettingsSet `
            -RunOnlyIfNetworkAvailable `
            -StartWhenAvailable `
            -ExecutionTimeLimit (New-TimeSpan -Minutes 30)

        $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest

        Register-ScheduledTask `
            -TaskName $TaskName `
            -Action $action `
            -Trigger $Trigger `
            -Settings $settings `
            -Principal $principal `
            -Force

        Write-Host "Registered: $TaskName ($ScheduleDescription)"
    }
}

# Daily disk report at 06:00
Register-MaintenanceTask `
    -TaskName "Daily-DiskReport" `
    -ScriptPath "$ScriptsFolder\Get-DiskReport.ps1" `
    -Arguments "-WarningThresholdPct 80 -OutputPath C:\Reports\disk-$(Get-Date -Format yyyyMMdd).csv" `
    -ScheduleDescription "Daily at 06:00" `
    -Trigger (New-ScheduledTaskTrigger -Daily -At "06:00")

# Weekly log cleanup on Sundays at 02:00
Register-MaintenanceTask `
    -TaskName "Weekly-LogCleanup" `
    -ScriptPath "$ScriptsFolder\Clear-OldLogs.ps1" `
    -Arguments "-LogDirectory D:\AppLogs -RetentionDays 30" `
    -ScheduleDescription "Sundays at 02:00" `
    -Trigger (New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At "02:00")

Write-Host ""
Write-Host "Registered tasks:"
Get-ScheduledTask | Where-Object TaskName -in @("Daily-DiskReport","Weekly-LogCleanup") |
    Select-Object TaskName, State, @{n="Schedule";e={($_.Triggers | Select-Object -First 1) -replace "MSFT_TaskTrigger"}} |
    Format-Table -AutoSize

🧪 Exercises

  1. Run Lab 1 on your machine. Pipe the results to Where-Object TotalGB -gt 10 to show only drives with meaningful data.
  2. Modify Lab 2 to accept a -ComputerName array and wrap the service check in Invoke-Command for remote fleet health checking.
  3. Create a test folder with files of different ages (use Set-ItemProperty to fake old timestamps) and run Lab 3 with -WhatIf first, then without.
  4. Add a Send-MailMessage (or Webhook) call to Lab 1 that fires only when warnings are found.
  5. Extend Lab 4 to also register a monthly task that runs a service health check and exports JSON.
🎮
Final Challenge

Combine all four labs into a single Invoke-SystemHealthReport.ps1 that: collects disk usage, service health, and log file counts, builds one consolidated [PSCustomObject] per machine, exports to JSON, and if any health check fails, emits a non-zero exit code. This becomes your go-to server health script in every environment.