Add scheduled maintenance, PC list export, and subnet scanner scripts
- Schedule-Maintenance.ps1: DPAPI credential storage + Task Scheduler integration - Export-PCList.ps1: Pull PC lists from ShopDB API with type/BU filtering - Find-ShopfloorPCs.ps1: Parallel subnet scanner with WinRM and DNS checks - INSTRUCTIONS.txt: Schedule-Maintenance.ps1 documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
223
remote-execution/Export-PCList.ps1
Normal file
223
remote-execution/Export-PCList.ps1
Normal file
@@ -0,0 +1,223 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Queries ShopDB API and exports a computer list for Invoke-RemoteMaintenance.ps1.
|
||||
|
||||
.DESCRIPTION
|
||||
Pulls shopfloor PC data from the ShopDB API and writes a text file
|
||||
(one hostname per line) compatible with:
|
||||
Invoke-RemoteMaintenance.ps1 -ComputerListFile "shopfloor-pcs.txt"
|
||||
|
||||
Can filter by PC type, business unit, or export all.
|
||||
|
||||
.PARAMETER PcType
|
||||
Filter by PC type. Omit for all types.
|
||||
|
||||
.PARAMETER BusinessUnit
|
||||
Filter by business unit. Omit for all BUs.
|
||||
|
||||
.PARAMETER OutputFile
|
||||
Output file path (default: shopfloor-pcs.txt in script directory).
|
||||
|
||||
.PARAMETER ApiUrl
|
||||
ShopDB API URL.
|
||||
|
||||
.PARAMETER IncludeDetails
|
||||
Add IP, PC type, and business unit as comments in the output.
|
||||
|
||||
.EXAMPLE
|
||||
.\Export-PCList.ps1
|
||||
# Export all shopfloor PCs
|
||||
|
||||
.EXAMPLE
|
||||
.\Export-PCList.ps1 -PcType Dashboard
|
||||
# Export only Dashboard PCs
|
||||
|
||||
.EXAMPLE
|
||||
.\Export-PCList.ps1 -BusinessUnit Blisk -OutputFile "blisk-pcs.txt"
|
||||
|
||||
.EXAMPLE
|
||||
.\Export-PCList.ps1 -PcType Dashboard,Shopfloor -BusinessUnit Blisk,HPT
|
||||
# Multiple filters (OR logic within each, AND between type/BU)
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet('Standard','Engineer','Shopfloor','CMM','Wax / Trace','Keyence',
|
||||
'Genspect','Heat Treat','Inspection','Dashboard','Lobby Display','Uncategorized')]
|
||||
[string[]]$PcType,
|
||||
|
||||
[ValidateSet('TBD','Blisk','HPT','Spools','Inspection','Venture','Turn/Burn','DT')]
|
||||
[string[]]$BusinessUnit,
|
||||
|
||||
[string]$OutputFile,
|
||||
|
||||
[string]$ApiUrl = "https://tsgwp00525.rd.ds.ge.com/shopdb/api.asp",
|
||||
|
||||
[switch]$IncludeDetails
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lookup tables (match IDs in ShopDB)
|
||||
# ---------------------------------------------------------------------------
|
||||
$PcTypeLookup = @{
|
||||
'Standard' = 1; 'Engineer' = 2; 'Shopfloor' = 3; 'Uncategorized' = 4;
|
||||
'CMM' = 5; 'Wax / Trace' = 6; 'Keyence' = 7; 'Genspect' = 8;
|
||||
'Heat Treat' = 9; 'Inspection' = 10; 'Dashboard' = 11; 'Lobby Display' = 12
|
||||
}
|
||||
|
||||
$PcTypeReverse = @{}
|
||||
$PcTypeLookup.GetEnumerator() | ForEach-Object { $PcTypeReverse[$_.Value] = $_.Key }
|
||||
|
||||
$BusinessUnitLookup = @{
|
||||
'TBD' = 1; 'Blisk' = 2; 'HPT' = 3; 'Spools' = 4;
|
||||
'Inspection' = 5; 'Venture' = 6; 'Turn/Burn' = 7; 'DT' = 8
|
||||
}
|
||||
|
||||
$BusinessUnitReverse = @{}
|
||||
$BusinessUnitLookup.GetEnumerator() | ForEach-Object { $BusinessUnitReverse[$_.Value] = $_.Key }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
function Write-Status {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$timestamp = Get-Date -Format "HH:mm:ss"
|
||||
$color = switch ($Level) {
|
||||
"OK" { "Green" }
|
||||
"WARN" { "Yellow" }
|
||||
"ERROR" { "Red" }
|
||||
default { "Cyan" }
|
||||
}
|
||||
Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color
|
||||
}
|
||||
|
||||
function Get-ShopfloorPCsFromApi {
|
||||
param(
|
||||
[string]$Url,
|
||||
[int]$PcTypeId = 0,
|
||||
[int]$BusinessUnitId = 0
|
||||
)
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
$queryParams = "action=getShopfloorPCs"
|
||||
if ($PcTypeId -gt 0) {
|
||||
$queryParams += "&pctypeid=$PcTypeId"
|
||||
}
|
||||
if ($BusinessUnitId -gt 0) {
|
||||
$queryParams += "&businessunitid=$BusinessUnitId"
|
||||
}
|
||||
|
||||
$fullUrl = "$Url`?$queryParams"
|
||||
$webClient = New-Object System.Net.WebClient
|
||||
$json = $webClient.DownloadString($fullUrl)
|
||||
$response = $json | ConvertFrom-Json
|
||||
|
||||
if ($response.success -and $response.data) {
|
||||
return $response.data
|
||||
}
|
||||
return @()
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Default output path
|
||||
# ---------------------------------------------------------------------------
|
||||
if (-not $OutputFile) {
|
||||
$OutputFile = Join-Path $PSScriptRoot "shopfloor-pcs.txt"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Query API
|
||||
# ---------------------------------------------------------------------------
|
||||
Write-Status "Querying ShopDB API..."
|
||||
|
||||
try {
|
||||
$allPCs = Get-ShopfloorPCsFromApi -Url $ApiUrl
|
||||
} catch {
|
||||
Write-Status "Failed to query API: $_" -Level "ERROR"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($allPCs.Count -eq 0) {
|
||||
Write-Status "API returned no PCs." -Level "ERROR"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Status "API returned $($allPCs.Count) total PCs" -Level "OK"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Filter
|
||||
# ---------------------------------------------------------------------------
|
||||
$filtered = $allPCs
|
||||
|
||||
if ($PcType) {
|
||||
$typeIds = $PcType | ForEach-Object { $PcTypeLookup[$_] }
|
||||
$filtered = $filtered | Where-Object { $_.pctypeid -in $typeIds }
|
||||
Write-Status "Filtered to PC types: $($PcType -join ', ') -> $($filtered.Count) PCs"
|
||||
}
|
||||
|
||||
if ($BusinessUnit) {
|
||||
$buIds = $BusinessUnit | ForEach-Object { $BusinessUnitLookup[$_] }
|
||||
$filtered = $filtered | Where-Object { $_.businessunitid -in $buIds }
|
||||
Write-Status "Filtered to business units: $($BusinessUnit -join ', ') -> $($filtered.Count) PCs"
|
||||
}
|
||||
|
||||
$filtered = @($filtered | Where-Object { $_.hostname })
|
||||
|
||||
if ($filtered.Count -eq 0) {
|
||||
Write-Status "No PCs match the selected filters." -Level "WARN"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Sort by hostname
|
||||
$filtered = $filtered | Sort-Object hostname
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Write output
|
||||
# ---------------------------------------------------------------------------
|
||||
$lines = [System.Collections.Generic.List[string]]::new()
|
||||
$lines.Add("# Shopfloor PC List - Generated $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
|
||||
$lines.Add("# Source: ShopDB API")
|
||||
|
||||
if ($PcType) {
|
||||
$lines.Add("# PC Type filter: $($PcType -join ', ')")
|
||||
}
|
||||
if ($BusinessUnit) {
|
||||
$lines.Add("# Business Unit filter: $($BusinessUnit -join ', ')")
|
||||
}
|
||||
|
||||
$lines.Add("# Total: $($filtered.Count) PCs")
|
||||
$lines.Add("# Usage: .\Invoke-RemoteMaintenance.ps1 -ComputerListFile `"$OutputFile`" -Task Reboot")
|
||||
$lines.Add("")
|
||||
|
||||
foreach ($pc in $filtered) {
|
||||
if ($IncludeDetails) {
|
||||
$typeName = $PcTypeReverse[[int]$pc.pctypeid]
|
||||
$buName = $BusinessUnitReverse[[int]$pc.businessunitid]
|
||||
$ip = if ($pc.ipaddress) { $pc.ipaddress } else { "no IP" }
|
||||
$lines.Add("$($pc.hostname) # $ip | $typeName | $buName")
|
||||
} else {
|
||||
$lines.Add($pc.hostname)
|
||||
}
|
||||
}
|
||||
|
||||
$lines | Out-File -FilePath $OutputFile -Encoding UTF8
|
||||
|
||||
Write-Status "Wrote $($filtered.Count) PCs to: $OutputFile" -Level "OK"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
Write-Host ""
|
||||
Write-Host "=== PC LIST ===" -ForegroundColor White
|
||||
|
||||
# Group by type for summary
|
||||
$byType = $filtered | Group-Object pctypeid | Sort-Object Name
|
||||
foreach ($group in $byType) {
|
||||
$typeName = $PcTypeReverse[[int]$group.Name]
|
||||
if (-not $typeName) { $typeName = "Unknown ($($group.Name))" }
|
||||
Write-Host " $($typeName): $($group.Count)" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Output file: $OutputFile" -ForegroundColor Green
|
||||
Write-Host "Next step: .\Invoke-RemoteMaintenance.ps1 -ComputerListFile `"$OutputFile`" -Task Reboot" -ForegroundColor Yellow
|
||||
266
remote-execution/Find-ShopfloorPCs.ps1
Normal file
266
remote-execution/Find-ShopfloorPCs.ps1
Normal file
@@ -0,0 +1,266 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Scans a subnet for live hosts and tests WinRM connectivity to build a
|
||||
usable computer list for Invoke-RemoteMaintenance.ps1.
|
||||
|
||||
.DESCRIPTION
|
||||
1. Pings all IPs in the specified subnet to find responsive hosts.
|
||||
2. Tests WinRM (TCP 5985) on each responsive host.
|
||||
3. Attempts to resolve hostnames via reverse DNS.
|
||||
4. Outputs a text file (one host per line) compatible with
|
||||
Invoke-RemoteMaintenance.ps1 -ComputerListFile.
|
||||
|
||||
.PARAMETER Subnet
|
||||
CIDR notation subnet to scan (default: 10.134.48.0/23).
|
||||
|
||||
.PARAMETER OutputFile
|
||||
Path for the generated computer list (default: shopfloor-pcs.txt in script dir).
|
||||
|
||||
.PARAMETER PingTimeout
|
||||
Ping timeout in milliseconds (default: 500).
|
||||
|
||||
.PARAMETER WinRMPort
|
||||
WinRM port to test (default: 5985).
|
||||
|
||||
.PARAMETER WinRMTimeout
|
||||
TCP connection timeout in milliseconds for WinRM test (default: 1000).
|
||||
|
||||
.PARAMETER ThrottleLimit
|
||||
Max concurrent ping/test jobs (default: 50).
|
||||
|
||||
.PARAMETER SkipWinRM
|
||||
Only do ping sweep, skip WinRM test.
|
||||
|
||||
.EXAMPLE
|
||||
.\Find-ShopfloorPCs.ps1
|
||||
# Scans 10.134.48.0/23, outputs shopfloor-pcs.txt
|
||||
|
||||
.EXAMPLE
|
||||
.\Find-ShopfloorPCs.ps1 -Subnet "10.134.48.0/23" -OutputFile "C:\temp\pcs.txt"
|
||||
|
||||
.EXAMPLE
|
||||
.\Find-ShopfloorPCs.ps1 -SkipWinRM
|
||||
# Ping sweep only, no WinRM check
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Subnet = "10.134.48.0/23",
|
||||
|
||||
[string]$OutputFile,
|
||||
|
||||
[int]$PingTimeout = 500,
|
||||
|
||||
[int]$WinRMPort = 5985,
|
||||
|
||||
[int]$WinRMTimeout = 1000,
|
||||
|
||||
[int]$ThrottleLimit = 50,
|
||||
|
||||
[switch]$SkipWinRM
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
function Write-Status {
|
||||
param([string]$Message, [string]$Level = "INFO")
|
||||
$timestamp = Get-Date -Format "HH:mm:ss"
|
||||
$color = switch ($Level) {
|
||||
"OK" { "Green" }
|
||||
"WARN" { "Yellow" }
|
||||
"ERROR" { "Red" }
|
||||
"SKIP" { "DarkGray"}
|
||||
default { "Cyan" }
|
||||
}
|
||||
Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color
|
||||
}
|
||||
|
||||
function ConvertFrom-CIDR {
|
||||
<# Returns an array of IP address strings for the given CIDR block.
|
||||
Excludes network and broadcast addresses. #>
|
||||
param([string]$CIDR)
|
||||
|
||||
$parts = $CIDR -split '/'
|
||||
$networkIP = [System.Net.IPAddress]::Parse($parts[0])
|
||||
$prefixLen = [int]$parts[1]
|
||||
|
||||
$ipBytes = $networkIP.GetAddressBytes()
|
||||
# .NET gives bytes in network order (big-endian)
|
||||
[Array]::Reverse($ipBytes)
|
||||
$ipUint = [BitConverter]::ToUInt32($ipBytes, 0)
|
||||
|
||||
$hostBits = 32 - $prefixLen
|
||||
$numAddresses = [math]::Pow(2, $hostBits)
|
||||
$networkAddr = $ipUint -band ([uint32]::MaxValue -shl $hostBits)
|
||||
|
||||
$ips = [System.Collections.Generic.List[string]]::new()
|
||||
# Skip .0 (network) and last (broadcast)
|
||||
for ($i = 1; $i -lt ($numAddresses - 1); $i++) {
|
||||
$addr = $networkAddr + $i
|
||||
$bytes = [BitConverter]::GetBytes([uint32]$addr)
|
||||
[Array]::Reverse($bytes)
|
||||
$ips.Add(("{0}.{1}.{2}.{3}" -f $bytes[0], $bytes[1], $bytes[2], $bytes[3]))
|
||||
}
|
||||
return $ips
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Default output path
|
||||
# ---------------------------------------------------------------------------
|
||||
if (-not $OutputFile) {
|
||||
$OutputFile = Join-Path $PSScriptRoot "shopfloor-pcs.txt"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 1: Generate IP list
|
||||
# ---------------------------------------------------------------------------
|
||||
Write-Status "Generating IP list for $Subnet"
|
||||
$allIPs = ConvertFrom-CIDR -CIDR $Subnet
|
||||
Write-Status "$($allIPs.Count) host addresses to scan"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 2: Ping sweep (parallel)
|
||||
# ---------------------------------------------------------------------------
|
||||
Write-Status "Starting ping sweep (timeout: ${PingTimeout}ms, threads: $ThrottleLimit)..."
|
||||
|
||||
$pingResults = $allIPs | ForEach-Object -ThrottleLimit $ThrottleLimit -Parallel {
|
||||
$ip = $_
|
||||
$ping = New-Object System.Net.NetworkInformation.Ping
|
||||
try {
|
||||
$reply = $ping.Send($ip, $using:PingTimeout)
|
||||
if ($reply.Status -eq 'Success') {
|
||||
[PSCustomObject]@{
|
||||
IP = $ip
|
||||
Alive = $true
|
||||
RoundTrip = $reply.RoundtripTime
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
# Silently skip unreachable hosts
|
||||
} finally {
|
||||
$ping.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
$aliveHosts = @($pingResults | Where-Object { $_.Alive })
|
||||
Write-Status "$($aliveHosts.Count) hosts responded to ping" -Level "OK"
|
||||
|
||||
if ($aliveHosts.Count -eq 0) {
|
||||
Write-Status "No hosts found. Check subnet and network connectivity." -Level "ERROR"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 3: WinRM port test (parallel)
|
||||
# ---------------------------------------------------------------------------
|
||||
if ($SkipWinRM) {
|
||||
Write-Status "Skipping WinRM test (-SkipWinRM)" -Level "SKIP"
|
||||
$winrmHosts = $aliveHosts
|
||||
} else {
|
||||
Write-Status "Testing WinRM (port $WinRMPort) on $($aliveHosts.Count) hosts..."
|
||||
|
||||
$winrmResults = $aliveHosts | ForEach-Object -ThrottleLimit $ThrottleLimit -Parallel {
|
||||
$ip = $_.IP
|
||||
$roundTrip = $_.RoundTrip
|
||||
$port = $using:WinRMPort
|
||||
$timeout = $using:WinRMTimeout
|
||||
|
||||
$tcp = New-Object System.Net.Sockets.TcpClient
|
||||
try {
|
||||
$task = $tcp.ConnectAsync($ip, $port)
|
||||
$connected = $task.Wait($timeout)
|
||||
if ($connected -and $tcp.Connected) {
|
||||
[PSCustomObject]@{
|
||||
IP = $ip
|
||||
WinRM = $true
|
||||
RoundTrip = $roundTrip
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
# Port closed or filtered
|
||||
} finally {
|
||||
$tcp.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
$winrmHosts = @($winrmResults | Where-Object { $_.WinRM })
|
||||
$noWinRM = $aliveHosts.Count - $winrmHosts.Count
|
||||
Write-Status "$($winrmHosts.Count) hosts have WinRM open, $noWinRM alive but no WinRM" -Level "OK"
|
||||
}
|
||||
|
||||
if ($winrmHosts.Count -eq 0) {
|
||||
Write-Status "No hosts with WinRM found." -Level "ERROR"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 4: Reverse DNS resolution
|
||||
# ---------------------------------------------------------------------------
|
||||
Write-Status "Resolving hostnames via reverse DNS..."
|
||||
|
||||
$resolvedHosts = $winrmHosts | ForEach-Object -ThrottleLimit $ThrottleLimit -Parallel {
|
||||
$ip = $_.IP
|
||||
$roundTrip = $_.RoundTrip
|
||||
$hostname = $null
|
||||
try {
|
||||
$entry = [System.Net.Dns]::GetHostEntry($ip)
|
||||
if ($entry.HostName -and $entry.HostName -ne $ip) {
|
||||
$hostname = $entry.HostName
|
||||
}
|
||||
} catch {
|
||||
# No reverse DNS
|
||||
}
|
||||
[PSCustomObject]@{
|
||||
IP = $ip
|
||||
Hostname = $hostname
|
||||
RoundTrip = $roundTrip
|
||||
}
|
||||
}
|
||||
|
||||
$withDNS = @($resolvedHosts | Where-Object { $_.Hostname })
|
||||
$withoutDNS = @($resolvedHosts | Where-Object { -not $_.Hostname })
|
||||
Write-Status "$($withDNS.Count) resolved via DNS, $($withoutDNS.Count) IP-only" -Level "OK"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 5: Write output file
|
||||
# ---------------------------------------------------------------------------
|
||||
$sortedHosts = $resolvedHosts | Sort-Object { [Version]($_.IP -replace '(\d+)\.(\d+)\.(\d+)\.(\d+)', '$1.$2.$3.$4') }
|
||||
|
||||
$lines = [System.Collections.Generic.List[string]]::new()
|
||||
$lines.Add("# Shopfloor PC List - Auto-generated $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
|
||||
$lines.Add("# Subnet: $Subnet")
|
||||
$lines.Add("# Hosts scanned: $($allIPs.Count) | Alive: $($aliveHosts.Count) | WinRM: $($winrmHosts.Count)")
|
||||
$lines.Add("# Usage: .\Invoke-RemoteMaintenance.ps1 -ComputerListFile `"$OutputFile`" -Task Reboot")
|
||||
$lines.Add("#")
|
||||
$lines.Add("# Format: hostname or IP (one per line). Lines starting with # are ignored.")
|
||||
$lines.Add("")
|
||||
|
||||
foreach ($h in $sortedHosts) {
|
||||
if ($h.Hostname) {
|
||||
# Use short hostname (strip domain suffix) as the entry, comment with IP
|
||||
$shortName = ($h.Hostname -split '\.')[0]
|
||||
$lines.Add("$shortName # $($h.IP) ($($h.RoundTrip)ms)")
|
||||
} else {
|
||||
$lines.Add("$($h.IP) # no DNS ($($h.RoundTrip)ms)")
|
||||
}
|
||||
}
|
||||
|
||||
$lines | Out-File -FilePath $OutputFile -Encoding UTF8
|
||||
Write-Status "Wrote $($winrmHosts.Count) entries to: $OutputFile" -Level "OK"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary table
|
||||
# ---------------------------------------------------------------------------
|
||||
Write-Host ""
|
||||
Write-Host "=== SCAN RESULTS ===" -ForegroundColor White
|
||||
Write-Host ("{0,-30} {1,-18} {2,8}" -f "HOSTNAME", "IP", "PING(ms)") -ForegroundColor DarkGray
|
||||
Write-Host ("-" * 58) -ForegroundColor DarkGray
|
||||
foreach ($h in $sortedHosts) {
|
||||
$name = if ($h.Hostname) { ($h.Hostname -split '\.')[0] } else { "(no DNS)" }
|
||||
Write-Host ("{0,-30} {1,-18} {2,8}" -f $name, $h.IP, $h.RoundTrip)
|
||||
}
|
||||
Write-Host ""
|
||||
Write-Host "Output file: $OutputFile" -ForegroundColor Green
|
||||
Write-Host "Next step: .\Invoke-RemoteMaintenance.ps1 -ComputerListFile `"$OutputFile`" -Task Reboot" -ForegroundColor Yellow
|
||||
100
remote-execution/INSTRUCTIONS.txt
Normal file
100
remote-execution/INSTRUCTIONS.txt
Normal file
@@ -0,0 +1,100 @@
|
||||
============================================================
|
||||
Schedule-Maintenance.ps1 - Documentation
|
||||
============================================================
|
||||
|
||||
Wrapper script that stores credentials securely and
|
||||
runs Invoke-RemoteMaintenance.ps1 unattended or on a
|
||||
schedule via Windows Task Scheduler.
|
||||
|
||||
|
||||
============================================================
|
||||
REQUIREMENTS
|
||||
============================================================
|
||||
|
||||
- PowerShell 5.1+
|
||||
- Run as Administrator (required for scheduling only)
|
||||
- Invoke-RemoteMaintenance.ps1 in the same folder
|
||||
- A PC list text file (one hostname per line)
|
||||
|
||||
|
||||
============================================================
|
||||
PARAMETERS
|
||||
============================================================
|
||||
|
||||
-SaveCredential Save credentials for unattended use
|
||||
-Username Domain\username (use with -SaveCredential)
|
||||
-Password Password (use with -SaveCredential)
|
||||
-Task Maintenance task name (e.g. Reboot)
|
||||
-ComputerListFile Path to text file with PC hostnames
|
||||
-CreateScheduledTask Register a Windows Scheduled Task
|
||||
-TaskFrequency Daily, Weekly, or Once (default: Weekly)
|
||||
-TaskDay Day of week (default: Sunday)
|
||||
-TaskTime Time in HH:mm format (default: 03:00)
|
||||
-TaskDate Specific date for Once (e.g. 2026-02-22)
|
||||
|
||||
|
||||
============================================================
|
||||
USAGE
|
||||
============================================================
|
||||
|
||||
1. SAVE CREDENTIALS (one time, does not require admin)
|
||||
|
||||
.\Schedule-Maintenance.ps1 -SaveCredential -Username "DS\570005354" -Password "MyP@ssw0rd"
|
||||
|
||||
- Encrypted with Windows DPAPI
|
||||
- Only your user account on this machine can decrypt
|
||||
- Re-run if your password changes
|
||||
|
||||
|
||||
2. RUN IMMEDIATELY (does not require admin)
|
||||
|
||||
.\Schedule-Maintenance.ps1 -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot
|
||||
|
||||
|
||||
3. SCHEDULE A ONE-TIME TASK (requires admin)
|
||||
|
||||
# Reboot one PC today at 3:00 PM
|
||||
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\test-reboot.txt" -Task Reboot -TaskFrequency Once -TaskTime "15:00" -TaskDate "2026-02-19"
|
||||
|
||||
# Reboot all PCs Sunday Feb 22 at 12:01 AM
|
||||
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot -TaskFrequency Once -TaskTime "00:01" -TaskDate "2026-02-22"
|
||||
|
||||
|
||||
4. SCHEDULE A RECURRING TASK (requires admin)
|
||||
|
||||
# Every Sunday at 12:01 AM
|
||||
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot -TaskFrequency Weekly -TaskDay Sunday -TaskTime "00:01"
|
||||
|
||||
# Every day at 2:00 AM
|
||||
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task DiskCleanup -TaskFrequency Daily -TaskTime "02:00"
|
||||
|
||||
|
||||
5. MANAGE SCHEDULED TASKS
|
||||
|
||||
Get-ScheduledTask | Where-Object { $_.TaskName -like "ShopfloorMaintenance*" }
|
||||
Start-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot"
|
||||
Unregister-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot"
|
||||
|
||||
|
||||
============================================================
|
||||
LOGS
|
||||
============================================================
|
||||
|
||||
.\logs\maintenance-YYYY-MM-DD_HHMMSS-TaskName.log
|
||||
|
||||
|
||||
============================================================
|
||||
TROUBLESHOOTING
|
||||
============================================================
|
||||
|
||||
"No saved credentials found"
|
||||
-> Run -SaveCredential with -Username and -Password
|
||||
|
||||
"Access is denied" when scheduling
|
||||
-> Right-click PowerShell -> Run as Administrator
|
||||
|
||||
"No credentials provided. Exiting."
|
||||
-> GUI prompt failed. Use -Username and -Password flags
|
||||
|
||||
Password changed
|
||||
-> Re-run -SaveCredential with new password
|
||||
208
remote-execution/Schedule-Maintenance.ps1
Normal file
208
remote-execution/Schedule-Maintenance.ps1
Normal file
@@ -0,0 +1,208 @@
|
||||
<#
|
||||
.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
|
||||
)
|
||||
|
||||
$credFile = Join-Path $PSScriptRoot ".maintenance-cred.xml"
|
||||
$scriptDir = $PSScriptRoot
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Save credentials (DPAPI - only decryptable by this user on this machine)
|
||||
# ---------------------------------------------------------------------------
|
||||
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
|
||||
}
|
||||
$cred | Export-Clixml -Path $credFile
|
||||
Write-Host "Credentials saved to: $credFile" -ForegroundColor Green
|
||||
Write-Host "Encrypted with DPAPI - only YOUR user account on THIS machine can decrypt them." -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 $credFile)) {
|
||||
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
|
||||
if (-not (Test-Path $credFile)) {
|
||||
Write-Host "ERROR: No saved credentials found at $credFile" -ForegroundColor Red
|
||||
Write-Host "Run with -SaveCredential first to store credentials." -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
try {
|
||||
$cred = Import-Clixml -Path $credFile
|
||||
Write-Host "Loaded saved credentials for: $($cred.UserName)" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "ERROR: Failed to load credentials: $_" -ForegroundColor Red
|
||||
Write-Host "Re-run with -SaveCredential to save new credentials." -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 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"
|
||||
|
||||
& $mainScript -ComputerListFile $ComputerListFile -Task $Task -Credential $cred 2>&1 | Tee-Object -FilePath $logFile
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Complete. Log saved to: $logFile" -ForegroundColor Green
|
||||
Reference in New Issue
Block a user