- NetworkDriveManager.ps1: S: drive repair utility - winrm-setup-package: Invoke-RemoteTask helper + Setup-WinRM.bat + HTML guide - remote-execution/udc: UDC_Update.ps1 and batch wrappers for updating DNC controllers on shop-floor PCs - Invoke-RemoteMaintenance.ps1: substantial rework (~1650 lines) - Schedule-Maintenance and complete-asset minor updates - Bump edncfix gitlink to v1.6.0 (2748bfa) - .gitignore: block inventory.csv/xlsx (CUI) and logs_*.txt (per-host logs) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
268 lines
11 KiB
PowerShell
268 lines
11 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Wrapper for running Invoke-RemoteMaintenance.ps1 unattended via Task Scheduler.
|
|
|
|
.DESCRIPTION
|
|
Stores credentials securely using Windows DPAPI (encrypted to your user account)
|
|
and runs maintenance tasks without interactive prompts.
|
|
|
|
First run: use -SaveCredential to store your username/password.
|
|
Subsequent runs (or Task Scheduler): credentials load automatically.
|
|
|
|
.PARAMETER SaveCredential
|
|
Prompt to save credentials for future unattended use.
|
|
|
|
.PARAMETER Task
|
|
Maintenance task to run (passed to Invoke-RemoteMaintenance.ps1).
|
|
|
|
.PARAMETER ComputerListFile
|
|
Path to PC list file (passed to Invoke-RemoteMaintenance.ps1).
|
|
|
|
.PARAMETER CreateScheduledTask
|
|
Register a Windows Scheduled Task to run this script on a schedule.
|
|
|
|
.PARAMETER TaskTime
|
|
Time for the scheduled task (default: 03:00 AM).
|
|
|
|
.PARAMETER TaskFrequency
|
|
How often to run: Daily, Weekly, or Once (default: Weekly).
|
|
|
|
.PARAMETER TaskDay
|
|
Day of week for Weekly frequency (default: Sunday).
|
|
|
|
.EXAMPLE
|
|
# Step 1: Save your credentials (one time, interactive)
|
|
.\Schedule-Maintenance.ps1 -SaveCredential
|
|
|
|
.EXAMPLE
|
|
# Step 2: Test unattended run
|
|
.\Schedule-Maintenance.ps1 -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot
|
|
|
|
.EXAMPLE
|
|
# Step 3: Create a scheduled task for weekly reboots
|
|
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot -TaskFrequency Weekly -TaskDay Sunday -TaskTime "03:00"
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[switch]$SaveCredential,
|
|
|
|
[string]$Task,
|
|
|
|
[string]$ComputerListFile,
|
|
|
|
[switch]$CreateScheduledTask,
|
|
|
|
[string]$TaskTime = "03:00",
|
|
|
|
[ValidateSet('Daily','Weekly','Once')]
|
|
[string]$TaskFrequency = "Weekly",
|
|
|
|
[ValidateSet('Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday')]
|
|
[string]$TaskDay = "Sunday",
|
|
|
|
[string]$TaskDate,
|
|
|
|
[string]$Username,
|
|
|
|
[string]$Password
|
|
)
|
|
|
|
$credDir = Join-Path $PSScriptRoot ".creds"
|
|
$keyFile = Join-Path $credDir "aes.key"
|
|
$userFile = Join-Path $credDir "username.txt"
|
|
$passFile = Join-Path $credDir "password.txt"
|
|
$scriptDir = $PSScriptRoot
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helper: load or create AES key (works across all user contexts on this PC)
|
|
# ---------------------------------------------------------------------------
|
|
function Get-AESKey {
|
|
if (-not (Test-Path $credDir)) {
|
|
New-Item -Path $credDir -ItemType Directory -Force | Out-Null
|
|
}
|
|
if (Test-Path $keyFile) {
|
|
return [byte[]](Get-Content $keyFile)
|
|
}
|
|
$key = New-Object byte[] 32
|
|
[System.Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($key)
|
|
$key | Set-Content $keyFile
|
|
return $key
|
|
}
|
|
|
|
function Load-SavedCredential {
|
|
if (-not (Test-Path $userFile) -or -not (Test-Path $passFile)) {
|
|
return $null
|
|
}
|
|
$key = Get-AESKey
|
|
$user = Get-Content $userFile
|
|
$encPass = Get-Content $passFile
|
|
$secPass = $encPass | ConvertTo-SecureString -Key $key
|
|
return New-Object System.Management.Automation.PSCredential($user, $secPass)
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Save credentials (AES key file - works from any user context on this PC)
|
|
# ---------------------------------------------------------------------------
|
|
if ($SaveCredential) {
|
|
if ($Username -and $Password) {
|
|
$secPass = ConvertTo-SecureString $Password -AsPlainText -Force
|
|
$cred = New-Object System.Management.Automation.PSCredential($Username, $secPass)
|
|
} else {
|
|
Write-Host "Enter the credentials used to connect to remote shopfloor PCs:" -ForegroundColor Cyan
|
|
Write-Host "If no prompt appears, re-run with: -Username 'DOMAIN\user' -Password 'pass'" -ForegroundColor Yellow
|
|
$cred = Get-Credential -Message "Enter admin credentials for remote shopfloor PCs"
|
|
}
|
|
if (-not $cred) {
|
|
Write-Host "No credentials provided. Exiting." -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
$key = Get-AESKey
|
|
$cred.UserName | Set-Content $userFile
|
|
$cred.Password | ConvertFrom-SecureString -Key $key | Set-Content $passFile
|
|
Write-Host "Credentials saved to: $credDir" -ForegroundColor Green
|
|
Write-Host "Encrypted with AES-256 key - works from any user context (normal or admin)." -ForegroundColor Yellow
|
|
Write-Host ""
|
|
Write-Host "You can now run tasks unattended:" -ForegroundColor Cyan
|
|
Write-Host " .\Schedule-Maintenance.ps1 -ComputerListFile '.\shopfloor-pcs.txt' -Task Reboot"
|
|
exit 0
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Create Scheduled Task
|
|
# ---------------------------------------------------------------------------
|
|
if ($CreateScheduledTask) {
|
|
if (-not $Task) {
|
|
Write-Host "ERROR: -Task is required when creating a scheduled task." -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
if (-not $ComputerListFile) {
|
|
Write-Host "ERROR: -ComputerListFile is required when creating a scheduled task." -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
|
|
# Resolve to absolute paths
|
|
$absListFile = (Resolve-Path $ComputerListFile -ErrorAction Stop).Path
|
|
$absScript = Join-Path $scriptDir "Schedule-Maintenance.ps1"
|
|
|
|
if (-not (Test-Path $passFile)) {
|
|
Write-Host "ERROR: No saved credentials found. Run with -SaveCredential first." -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
|
|
$taskName = "ShopfloorMaintenance-$Task"
|
|
$psArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$absScript`" -Task `"$Task`" -ComputerListFile `"$absListFile`""
|
|
|
|
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $psArgs -WorkingDirectory $scriptDir
|
|
|
|
$triggerTime = if ($TaskDate) {
|
|
[DateTime]::Parse("$TaskDate $TaskTime")
|
|
} else {
|
|
[DateTime]::Parse($TaskTime)
|
|
}
|
|
|
|
$trigger = switch ($TaskFrequency) {
|
|
'Daily' { New-ScheduledTaskTrigger -Daily -At $triggerTime }
|
|
'Weekly' { New-ScheduledTaskTrigger -Weekly -DaysOfWeek $TaskDay -At $triggerTime }
|
|
'Once' { New-ScheduledTaskTrigger -Once -At $triggerTime }
|
|
}
|
|
|
|
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -RunOnlyIfNetworkAvailable
|
|
|
|
# Register as current user (needed for DPAPI credential decryption)
|
|
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
|
Write-Host "Creating scheduled task '$taskName' as $currentUser..." -ForegroundColor Cyan
|
|
Write-Host " Schedule: $TaskFrequency at $TaskTime$(if ($TaskFrequency -eq 'Weekly') { " ($TaskDay)" })" -ForegroundColor Gray
|
|
Write-Host " Task: $Task" -ForegroundColor Gray
|
|
Write-Host " PC List: $absListFile" -ForegroundColor Gray
|
|
|
|
$existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
|
if ($existingTask) {
|
|
Write-Host "Task '$taskName' already exists. Updating..." -ForegroundColor Yellow
|
|
Set-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Settings $settings | Out-Null
|
|
} else {
|
|
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Settings $settings -User $currentUser -RunLevel Highest -Description "Shopfloor PC Maintenance - $Task (via Invoke-RemoteMaintenance.ps1)" | Out-Null
|
|
}
|
|
|
|
Write-Host "Scheduled task '$taskName' created successfully." -ForegroundColor Green
|
|
Write-Host ""
|
|
Write-Host "Manage in Task Scheduler or with:" -ForegroundColor Cyan
|
|
Write-Host " Get-ScheduledTask -TaskName '$taskName'"
|
|
Write-Host " Start-ScheduledTask -TaskName '$taskName' # run now"
|
|
Write-Host " Unregister-ScheduledTask -TaskName '$taskName' # delete"
|
|
exit 0
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Run maintenance (unattended)
|
|
# ---------------------------------------------------------------------------
|
|
if (-not $Task) {
|
|
Write-Host "ERROR: -Task is required. Example: -Task Reboot" -ForegroundColor Red
|
|
Write-Host " Or use -SaveCredential to store credentials first." -ForegroundColor Yellow
|
|
Write-Host " Or use -CreateScheduledTask to set up a scheduled run." -ForegroundColor Yellow
|
|
exit 1
|
|
}
|
|
|
|
if (-not $ComputerListFile) {
|
|
Write-Host "ERROR: -ComputerListFile is required." -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
|
|
# Load saved credentials
|
|
$cred = Load-SavedCredential
|
|
if (-not $cred) {
|
|
Write-Host "ERROR: No saved credentials found in $credDir" -ForegroundColor Red
|
|
Write-Host "Run with -SaveCredential first to store credentials." -ForegroundColor Yellow
|
|
exit 1
|
|
}
|
|
Write-Host "Loaded saved credentials for: $($cred.UserName)" -ForegroundColor Green
|
|
|
|
# Log output
|
|
$logDir = Join-Path $scriptDir "logs"
|
|
if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null }
|
|
$logFile = Join-Path $logDir "maintenance-$(Get-Date -Format 'yyyy-MM-dd_HHmmss')-$Task.log"
|
|
|
|
Write-Host "Running: Invoke-RemoteMaintenance.ps1 -Task $Task -ComputerListFile $ComputerListFile" -ForegroundColor Cyan
|
|
Write-Host "Log: $logFile" -ForegroundColor Gray
|
|
|
|
$mainScript = Join-Path $scriptDir "Invoke-RemoteMaintenance.ps1"
|
|
|
|
Start-Transcript -Path $logFile -Force | Out-Null
|
|
& $mainScript -ComputerListFile $ComputerListFile -Task $Task -Credential $cred
|
|
Stop-Transcript | Out-Null
|
|
|
|
# Parse log for results summary
|
|
$logContent = Get-Content $logFile -ErrorAction SilentlyContinue
|
|
$okPCs = @($logContent | Select-String '\[OK\]\s+(\S+)' | ForEach-Object { $_.Matches[0].Groups[1].Value })
|
|
$failPCs = @($logContent | Select-String '\[FAIL\]\s+(\S+)' | ForEach-Object { $_.Matches[0].Groups[1].Value } | Where-Object { $_ -ne ':' } | Sort-Object -Unique)
|
|
|
|
$summaryFile = Join-Path $logDir "LAST-RUN-SUMMARY.txt"
|
|
$summary = @()
|
|
$summary += "============================================"
|
|
$summary += " MAINTENANCE RESULTS - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
|
$summary += " Task: $Task"
|
|
$summary += "============================================"
|
|
$summary += ""
|
|
$summary += "SUCCEEDED: $($okPCs.Count)"
|
|
foreach ($pc in $okPCs) { $summary += " [OK] $pc" }
|
|
$summary += ""
|
|
$summary += "FAILED: $($failPCs.Count)"
|
|
foreach ($pc in $failPCs) { $summary += " [FAIL] $pc" }
|
|
$summary += ""
|
|
$summary += "Full log: $logFile"
|
|
|
|
$summary | Out-File -FilePath $summaryFile -Encoding UTF8
|
|
|
|
Write-Host ""
|
|
Write-Host "Complete. Log saved to: $logFile" -ForegroundColor Green
|
|
Write-Host ""
|
|
Write-Host "=== RESULTS ===" -ForegroundColor White
|
|
Write-Host " Succeeded: $($okPCs.Count)" -ForegroundColor Green
|
|
foreach ($pc in $okPCs) { Write-Host " $pc" -ForegroundColor Green }
|
|
if ($failPCs.Count -gt 0) {
|
|
Write-Host " Failed: $($failPCs.Count)" -ForegroundColor Red
|
|
foreach ($pc in $failPCs) { Write-Host " $pc" -ForegroundColor Red }
|
|
}
|
|
Write-Host ""
|
|
Write-Host "Summary also saved to: $summaryFile" -ForegroundColor Yellow
|