Generalize USMT Dash and add a containerized demo mode
Remove all environment-specific data and make the dashboard adoptable anywhere, then add a demo that runs the real PowerShell services. - Strip cached device data, internal knowledge-base articles and screenshots, server/site names and the old project notes; replace the knowledge base with generic sample articles and diagrams - Move every environment value into Config/usmt-dash.psd1 - Route all infrastructure access in the worker and Get-Software.ps1 through a provider (Providers/SCCM.ps1), with a config-driven asset inventory view instead of a hard-coded custom class - Add Providers/Demo.ps1: a deterministic simulated fleet whose migrations progress in real time, and Demo/New-DemoData.ps1 to seed it - Server: -Demo makes the API read-only; add get-config and get-logs; serve static files only from web/, Documentation/ and an allowlist of Data/ files with a path-traversal guard; gzip large responses - Mirror both services' console output to Data/Logs via a Write-Host proxy, and add a terminal panel to the dashboard that streams it - Demo sandbox (web/demo/demo.js): visitor changes to tags, articles and images are kept in the visitor's browser and echoed in the terminals; nothing a visitor does reaches the server - Vendor marked, highlight.js and html2pdf.js instead of loading CDNs - Dockerfile and compose file for hosting the demo (Coolify-ready)
This commit is contained in:
commit
1c531bc2ab
56 files changed
+10232
No files matched your search
@@ -0,0 +1,499 @@
|
||||
# =========================================================================================
|
||||
# Demo provider - simulated fleet
|
||||
#
|
||||
# Implements the same functions as Providers\SCCM.ps1, but every answer comes from a
|
||||
# deterministic model instead of SCCM / AD / DNS / SMB. A device's hardware, users,
|
||||
# location and software are derived from a hash of its name, and its migration moves
|
||||
# through each stage on a clock, so the unmodified worker loop sees devices go from
|
||||
# Not Started -> Backup In Progress -> Backup Completed -> Restore In Progress -> Completed.
|
||||
#
|
||||
# Naming scheme: PrvDT- / PubDT- (private/public desktops) PrvLT- / PubLT- (laptops)
|
||||
# followed by an 8 character serial. Destination (replacement) devices get a serial that
|
||||
# is derived reversibly from their source, so any process can tell which is which.
|
||||
# =========================================================================================
|
||||
|
||||
$script:DemoCfg = $config.Demo
|
||||
$script:DemoLifecycle = [double]$script:DemoCfg.LifecycleHours * 3600
|
||||
$script:DemoProfiles = @{}
|
||||
$script:DemoAlphabet = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ"
|
||||
$script:DemoAdminShare = 'C$'
|
||||
$script:DemoLogFolders = @('Windows\CCM\Logs\SMSTSLog', 'Windows\CCM\Logs')
|
||||
|
||||
# --- Stage boundaries (fraction of the lifecycle) ---
|
||||
$script:DemoStages = @(
|
||||
@{ Name = 'NotStarted'; Start = 0.00 }
|
||||
@{ Name = 'BackupInProgress'; Start = 0.25 }
|
||||
@{ Name = 'BackupCompleted'; Start = 0.40 }
|
||||
@{ Name = 'RestoreInProgress'; Start = 0.55 }
|
||||
@{ Name = 'Completed'; Start = 0.70 }
|
||||
)
|
||||
|
||||
# --- Catalogs ---
|
||||
$script:DemoSurnames = @("Smith","Johnson","Williams","Brown","Jones","Garcia","Miller","Davis","Rodriguez","Martinez","Hernandez","Lopez","Gonzalez","Wilson","Anderson","Thomas","Taylor","Moore","Jackson","Martin","Lee","Perez","Thompson","White","Harris","Sanchez","Clark","Ramirez","Lewis","Robinson","Walker","Young","Allen","King","Wright","Scott","Torres","Nguyen","Hill","Flores","Green","Adams","Nelson","Baker","Hall","Rivera","Campbell","Mitchell","Carter","Roberts","Patel","Kim","Chen","Shah","Murphy","Cook","Rogers","Morgan","Cooper","Reed")
|
||||
$script:DemoBuildings = @(
|
||||
@{ Name = "Main"; Code = "MN"; Net = 20 }
|
||||
@{ Name = "Annex"; Code = "AX"; Net = 30 }
|
||||
@{ Name = "North"; Code = "NO"; Net = 40 }
|
||||
@{ Name = "Warehouse"; Code = "WH"; Net = 50 }
|
||||
)
|
||||
$script:DemoDepartments = @(
|
||||
@{ Name = "Accounting"; Apps = @("QuickBooks","AcrobatPro","PowerBI","Excel Add-ins") }
|
||||
@{ Name = "Finance"; Apps = @("QuickBooks","PowerBI","Tableau","AcrobatPro") }
|
||||
@{ Name = "Sales"; Apps = @("Salesforce","Zoom","Slack","AcrobatPro") }
|
||||
@{ Name = "Marketing"; Apps = @("CreativeCloud","Snagit","Slack","Zoom") }
|
||||
@{ Name = "Human Resources"; Apps = @("AcrobatPro","Zoom","Snagit") }
|
||||
@{ Name = "IT"; Apps = @("NotepadPP","PuTTY","WinSCP","Wireshark","VSCode","SSMS","Python") }
|
||||
@{ Name = "Engineering"; Apps = @("AutoCAD","Bluebeam","Visio","Project","Python") }
|
||||
@{ Name = "Operations"; Apps = @("Project","Visio","Tableau","PowerBI") }
|
||||
@{ Name = "Legal"; Apps = @("AcrobatPro","Snagit","Zoom") }
|
||||
@{ Name = "Customer Support"; Apps = @("Salesforce","Zoom","Slack") }
|
||||
@{ Name = "Executive"; Apps = @("AcrobatPro","PowerBI","Zoom") }
|
||||
@{ Name = "Facilities"; Apps = @("AutoCAD","Visio","Bluebeam") }
|
||||
@{ Name = "Training"; Apps = @("Snagit","Camtasia","Zoom") }
|
||||
)
|
||||
$script:DemoModels = @{
|
||||
HP = @{ OldDT = "HP EliteDesk 800 G4 DM 65W"; NewDT = "HP Elite Mini 800 G9 Desktop PC"; OldLT = "HP EliteBook 840 G5"; NewLT = "HP EliteBook 840 G10 Notebook PC"; Tool = "HP Support Assistant"; ToolPub = "HP Inc." }
|
||||
Dell = @{ OldDT = "OptiPlex 7060"; NewDT = "OptiPlex Micro 7010"; OldLT = "Latitude 5490"; NewLT = "Latitude 7440"; Tool = "Dell Command | Update"; ToolPub = "Dell Inc." }
|
||||
Lenovo = @{ OldDT = "ThinkCentre M720q"; NewDT = "ThinkCentre M70q Gen 4"; OldLT = "ThinkPad T480"; NewLT = "ThinkPad T14 Gen 4"; Tool = "Lenovo Vantage Service"; ToolPub = "Lenovo Group Ltd." }
|
||||
}
|
||||
$script:DemoSsdModels = @("KXG50ZNV256G TOSHIBA", "SAMSUNG MZVLB256HBHQ-000H1", "SK hynix PC611 HFS256GD9TNG", "Micron 2450 NVMe 512GB", "SAMSUNG MZVL2512HCJQ-00BH1", "KBG50ZNS512G KIOXIA")
|
||||
$script:DemoMounts = @("Desktop Bundle", "Monitor Arm", "Under Desk", "Wall Mount", "Desktop Bundle", "Monitor Arm")
|
||||
$script:DemoNotes = @("N/A", "N/A", "N/A", "N/A", "Front counter", "Dual monitors", "Desk 3", "Shared with night shift", "Badge printer attached", "Label printer attached")
|
||||
|
||||
# Specialty apps: one or more ARP entries each (multi-entry apps exercise the "suite" rules)
|
||||
$script:DemoApps = @{
|
||||
QuickBooks = @(@("QuickBooks Desktop Pro 2024", "34.0.4004.2405", "Intuit Inc."), @("QuickBooks Database Server Manager 34.0", "34.0.4004.2405", "Intuit Inc."), @("Intuit Data Protect", "34.0.2000.1", "Intuit Inc."))
|
||||
AcrobatPro = @(, @("Adobe Acrobat (64-bit)", "25.001.20531", "Adobe"))
|
||||
PowerBI = @(, @("Microsoft Power BI Desktop (x64)", "2.140.1205.0", "Microsoft Corporation"))
|
||||
"Excel Add-ins" = @(, @("Power Pivot for Excel", "16.0.4849.1000", "Microsoft Corporation"))
|
||||
Tableau = @(, @("Tableau 2024.3 (20243.24.1112.0334)", "24.3.1112", "Tableau Software"))
|
||||
Salesforce = @(@("Salesforce for Outlook", "3.4.8.393", "salesforce.com"), @("Salesforce Data Loader", "62.0.0", "salesforce.com"))
|
||||
Zoom = @(, @("Zoom Workplace (64-bit)", "6.2.11.50443", "Zoom Video Communications, Inc."))
|
||||
Slack = @(, @("Slack (Machine - MSI)", "4.41.105", "Slack Technologies Inc."))
|
||||
CreativeCloud = @(@("Adobe Creative Cloud", "6.4.0.361", "Adobe Inc."), @("Adobe Photoshop 2025", "26.1.0.121", "Adobe Inc."), @("Adobe Illustrator 2025", "29.1", "Adobe Inc."))
|
||||
Snagit = @(, @("Snagit 2024", "24.3.0.4570", "TechSmith Corporation"))
|
||||
Camtasia = @(, @("Camtasia 2024", "24.1.5.6082", "TechSmith Corporation"))
|
||||
NotepadPP = @(, @("Notepad++ (64-bit x64)", "8.7.1", "Notepad++ Team"))
|
||||
PuTTY = @(, @("PuTTY release 0.81 (64-bit)", "0.81.0.0", "Simon Tatham"))
|
||||
WinSCP = @(, @("WinSCP 6.3.5", "6.3.5", "Martin Prikryl"))
|
||||
Wireshark = @(@("Wireshark 4.4.1 x64", "4.4.1", "The Wireshark developer community"), @("Npcap", "1.79", "Nmap Project"))
|
||||
VSCode = @(, @("Microsoft Visual Studio Code", "1.94.2", "Microsoft Corporation"))
|
||||
SSMS = @(@("SQL Server Management Studio", "20.2.30.0", "Microsoft Corp."), @("SSMS Post Install Tasks", "20.2.30.0", "Microsoft Corporation"))
|
||||
Python = @(@("Python 3.12.7 (64-bit)", "3.12.7150.0", "Python Software Foundation"), @("Python Launcher", "3.12.7150.0", "Python Software Foundation"))
|
||||
AutoCAD = @(, @("AutoCAD LT 2025", "25.0.58.0", "Autodesk"))
|
||||
Bluebeam = @(, @("Bluebeam Revu 21", "21.3.0", "Bluebeam, Inc."))
|
||||
Visio = @(, @("Microsoft Visio - en-us", "16.0.18129.20116", "Microsoft Corporation"))
|
||||
Project = @(, @("Microsoft Project - en-us", "16.0.18129.20116", "Microsoft Corporation"))
|
||||
}
|
||||
# Installed on every device - these match Blacklisted-Apps.txt so they never count as "specialty".
|
||||
# Each row is: name, version on old hardware, version on new hardware, publisher. (The leading
|
||||
# commas keep each row an array; without them PowerShell flattens the rows into one list.)
|
||||
$script:DemoBaseline = @(
|
||||
,@("Microsoft 365 Apps for enterprise - en-us", "16.0.17928.20156", "16.0.18129.20116", "Microsoft Corporation")
|
||||
,@("Microsoft Edge", "129.0.2792.89", "130.0.2849.46", "Microsoft Corporation")
|
||||
,@("Microsoft Edge WebView2 Runtime", "129.0.2792.89", "130.0.2849.46", "Microsoft Corporation")
|
||||
,@("Microsoft OneDrive", "24.180.0905.0002", "24.201.1006.0005", "Microsoft Corporation")
|
||||
,@("Microsoft Teams Meeting Add-in for Microsoft Office", "1.24.21705", "1.24.25804", "Microsoft")
|
||||
,@("Microsoft Visual C++ 2015-2022 Redistributable (x64) - 14.40.33810", "14.40.33810.0", "14.40.33816.0", "Microsoft Corporation")
|
||||
,@("Microsoft Visual C++ 2015-2022 Redistributable (x86) - 14.40.33810", "14.40.33810.0", "14.40.33816.0", "Microsoft Corporation")
|
||||
,@("Microsoft Intune Management Extension", "1.83.107.0", "1.85.103.0", "Microsoft Corporation")
|
||||
,@("Microsoft Policy Platform", "68.1.1010.0", "68.1.1010.0", "Microsoft Corporation")
|
||||
,@("Configuration Manager Client", "5.00.9128.1007", "5.00.9132.1011", "Microsoft Corporation")
|
||||
,@("Google Chrome", "129.0.6668.101", "130.0.6723.70", "Google LLC")
|
||||
,@("Adobe Acrobat Reader DC (64-bit)", "24.003.20180", "24.004.20219", "Adobe")
|
||||
,@("7-Zip 23.01 (x64)", "23.01", "24.08", "Igor Pavlov")
|
||||
,@("Endpoint Protection Agent", "7.18.18603", "7.19.18710", "Security Vendor")
|
||||
,@("Intel(R) Management Engine Components", "2228.5.4.0", "2412.5.69.0", "Intel Corporation")
|
||||
,@("Realtek Audio Driver", "6.0.9549.1", "6.0.9696.1", "Realtek Semiconductor Corp.")
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------------------
|
||||
# Deterministic randomness
|
||||
# -----------------------------------------------------------------------------------------
|
||||
function Get-DemoMul32([uint64]$a, [uint64]$c) {
|
||||
# (a * c) mod 2^32 without overflowing 64 bits
|
||||
$lo = $a * ($c % 65536)
|
||||
$hi = (($a * [math]::Floor($c / 65536)) % 65536) * 65536
|
||||
return [uint64](($lo + $hi) % 4294967296)
|
||||
}
|
||||
function Get-DemoHash([string]$s) {
|
||||
# FNV-1a, then the murmur3 finalizer so similar inputs land far apart
|
||||
[uint64]$h = 2166136261
|
||||
foreach ($b in [System.Text.Encoding]::UTF8.GetBytes($s.ToUpperInvariant())) {
|
||||
$h = (($h -bxor $b) * 16777619) % 4294967296
|
||||
}
|
||||
$h = $h -bxor [math]::Floor($h / 65536)
|
||||
$h = Get-DemoMul32 $h 2246822507
|
||||
$h = $h -bxor [math]::Floor($h / 8192)
|
||||
$h = Get-DemoMul32 $h 3266489909
|
||||
$h = $h -bxor [math]::Floor($h / 65536)
|
||||
return [uint64]$h
|
||||
}
|
||||
function Get-DemoRand([string]$name, [string]$salt) { return (Get-DemoHash "$name|$salt") / 4294967296.0 }
|
||||
function Get-DemoPick([string]$name, [string]$salt, [array]$list) { return $list[[int][math]::Floor((Get-DemoRand $name $salt) * $list.Count)] }
|
||||
function Get-DemoInt([string]$name, [string]$salt, [int]$min, [int]$max) { return $min + [int][math]::Floor((Get-DemoRand $name $salt) * ($max - $min + 1)) }
|
||||
function Get-DemoNow { return [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() }
|
||||
|
||||
# -----------------------------------------------------------------------------------------
|
||||
# Serial scheme: 7 chars + check char. Sources and destinations use different check offsets
|
||||
# and a destination's first 7 chars are a reversible scramble of its source's.
|
||||
# -----------------------------------------------------------------------------------------
|
||||
function Get-DemoCheckChar([string]$body, [int]$offset) {
|
||||
$a = $script:DemoAlphabet; $sum = 0
|
||||
for ($i = 0; $i -lt $body.Length; $i++) { $sum += ($a.IndexOf($body[$i]) + 1) * ($i + 1) }
|
||||
return $a[($sum + $offset) % $a.Length]
|
||||
}
|
||||
function New-DemoSerial([string]$seed) {
|
||||
$a = $script:DemoAlphabet; $body = ""
|
||||
for ($i = 0; $i -lt 7; $i++) { $body += $a[(Get-DemoInt $seed "s$i" 0 ($a.Length - 1))] }
|
||||
return $body + (Get-DemoCheckChar $body 0)
|
||||
}
|
||||
function Get-DemoDestName([string]$src) {
|
||||
$a = $script:DemoAlphabet
|
||||
$prefix, $serial = $src.ToUpper() -split '-', 2
|
||||
$body = ""
|
||||
foreach ($ch in $serial.Substring(0, 7).ToCharArray()) { $body = $a[($a.IndexOf($ch) * 7 + 11) % $a.Length] + $body }
|
||||
return "$prefix-$body$(Get-DemoCheckChar $body 17)"
|
||||
}
|
||||
function Get-DemoSourceName([string]$dst) {
|
||||
$a = $script:DemoAlphabet
|
||||
$prefix, $serial = $dst.ToUpper() -split '-', 2
|
||||
$body = ""
|
||||
foreach ($ch in $serial.Substring(0, 7).ToCharArray()) { $body = $a[((($a.IndexOf($ch) - 11) % $a.Length + $a.Length) * 5) % $a.Length] + $body }
|
||||
return "$prefix-$body$(Get-DemoCheckChar $body 0)"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------------------
|
||||
# Device profile (static facts about a device)
|
||||
# -----------------------------------------------------------------------------------------
|
||||
function Get-DemoProfile([string]$name) {
|
||||
$key = $name.ToUpper()
|
||||
if ($script:DemoProfiles.ContainsKey($key)) { return $script:DemoProfiles[$key] }
|
||||
|
||||
$p = @{ Name = $key; Valid = $false; IsDest = $false; Source = $key }
|
||||
if ($key -match '^(PRV|PUB)(DT|LT)-([0-9A-Z]{8})$') {
|
||||
$p.Valid = $true
|
||||
$p.Public = ($Matches[1] -eq 'PUB')
|
||||
$p.Laptop = ($Matches[2] -eq 'LT')
|
||||
$serial = $Matches[3]
|
||||
$body = $serial.Substring(0, 7)
|
||||
$p.IsDest = ($body.ToCharArray() | Where-Object { $script:DemoAlphabet.IndexOf($_) -lt 0 }).Count -eq 0 -and $serial[7] -eq (Get-DemoCheckChar $body 17)
|
||||
if ($p.IsDest) { $p.Source = Get-DemoSourceName $key }
|
||||
|
||||
# Facts shared by a source and its replacement come from the SOURCE name
|
||||
$s = $p.Source
|
||||
$p.Offset = (Get-DemoRand $s 'offset') * $script:DemoLifecycle
|
||||
$p.NoAssoc = (Get-DemoRand $s 'assoc') -lt 0.08
|
||||
$p.Failing = (Get-DemoRand $s 'fail') -lt 0.05
|
||||
$p.Vendor = Get-DemoPick $s 'vendor' @("HP", "HP", "Dell", "Lenovo")
|
||||
$p.Building = Get-DemoPick $s 'bldg' $script:DemoBuildings
|
||||
$p.Department = Get-DemoPick $s 'dept' $script:DemoDepartments
|
||||
$floor = Get-DemoInt $s 'floor' 1 5
|
||||
$room = Get-DemoInt $s 'room' 100 480
|
||||
$p.Location = "{0:D2}-{1}" -f $floor, $room
|
||||
$p.Vanity = if ((Get-DemoRand $s 'vanity') -lt 0.4) { "{0:D2}-{1}{2}" -f $floor, $room, (Get-DemoPick $s 'vsuf' @("A","B","C","D")) } else { "N/A" }
|
||||
$p.PhoneExt = "{0}" -f (Get-DemoInt $s 'ext' 40000 49999)
|
||||
$p.Mount = if ($p.Laptop) { "Docking Station" } else { Get-DemoPick $s 'mount' $script:DemoMounts }
|
||||
$rt = Get-DemoRand $s 'roomtype'
|
||||
$p.RoomType = if ($rt -lt 0.70) { "Standard" } else { Get-DemoPick $s 'rtl' @("Conference Room", "Hoteling Desk", "Training Room", "Reception", "Shared Space") }
|
||||
$p.User = ("abcdefghijklmnoprstw"[(Get-DemoInt $s 'fi' 0 19)]).ToString() + (Get-DemoPick $s 'sn' $script:DemoSurnames).ToLower()
|
||||
$p.InfoBy = (Get-DemoPick $s 'ib1' @("A","B","C","D","J","K","M","R","S","T")) + (Get-DemoPick $s 'ib2' @("B","C","G","H","L","M","P","R","S","W"))
|
||||
$p.Notes = Get-DemoPick $s 'notes' $script:DemoNotes
|
||||
$p.SrcFree = if ((Get-DemoRand $s 'lowdisk') -lt 0.08) { 4.0 + 12 * (Get-DemoRand $s 'freeL') } else { 30.0 + 150 * (Get-DemoRand $s 'free') }
|
||||
$p.Apps = @($p.Department.Apps | Where-Object { (Get-DemoRand $s "app$_") -lt 0.65 })
|
||||
if ($p.Apps.Count -eq 0) { $p.Apps = @($p.Department.Apps[0]) }
|
||||
|
||||
# Facts specific to this physical device
|
||||
$n = $key
|
||||
$p.Mac = (1..6 | ForEach-Object { "{0:X2}" -f (Get-DemoInt $n "mac$_" 0 255) }) -join ':'
|
||||
$p.IP = "10.{0}.{1}.{2}" -f $p.Building.Net, (Get-DemoInt $n 'ip3' 10 250), (Get-DemoInt $n 'ip4' 10 250)
|
||||
$form = if ($p.Laptop) { "LT" } else { "DT" }
|
||||
$p.Model = if ($p.IsDest) { $script:DemoModels[$p.Vendor]["New$form"] } else { $script:DemoModels[$p.Vendor]["Old$form"] }
|
||||
$p.RAM = if ($p.IsDest) { Get-DemoPick $n 'ram' @("15.7 GB", "15.7 GB", "31.7 GB") } else { Get-DemoPick $n 'ram' @("7.8 GB", "7.8 GB", "15.8 GB") }
|
||||
$p.DiskSize = if ($p.IsDest) { Get-DemoPick $n 'disk' @(476, 476, 953) } else { Get-DemoPick $n 'disk' @(238, 238, 476) }
|
||||
$p.DiskModel = Get-DemoPick $n 'ssd' $script:DemoSsdModels
|
||||
$p.DiskSerial = (1..8 | ForEach-Object { "{0:X4}" -f (Get-DemoInt $n "dsn$_" 0 65535) }) -join '_'
|
||||
$p.Inactive = (Get-DemoRand $n 'inactive') -lt 0.04
|
||||
}
|
||||
$script:DemoProfiles[$key] = $p
|
||||
return $p
|
||||
}
|
||||
|
||||
# Where a migration currently is. Both halves of a pair share the source's clock.
|
||||
function Get-DemoStage([hashtable]$p) {
|
||||
$now = Get-DemoNow
|
||||
$L = $script:DemoLifecycle
|
||||
$phase = (($now + $p.Offset) % $L) / $L
|
||||
$idx = 0
|
||||
for ($i = 0; $i -lt $script:DemoStages.Count; $i++) { if ($phase -ge $script:DemoStages[$i].Start) { $idx = $i } }
|
||||
$start = $script:DemoStages[$idx].Start
|
||||
$end = if ($idx -lt $script:DemoStages.Count - 1) { $script:DemoStages[$idx + 1].Start } else { 1.0 }
|
||||
$cycleStart = $now - ($phase * $L)
|
||||
return @{
|
||||
Name = $script:DemoStages[$idx].Name
|
||||
Phase = $phase
|
||||
Progress = ($phase - $start) / ($end - $start)
|
||||
StageStarted = [DateTimeOffset]::FromUnixTimeSeconds([long]($cycleStart + $start * $L)).LocalDateTime
|
||||
StageEnded = [DateTimeOffset]::FromUnixTimeSeconds([long]($cycleStart + $end * $L)).LocalDateTime
|
||||
}
|
||||
}
|
||||
|
||||
function Get-DemoNetworkState([string]$computer) {
|
||||
$p = Get-DemoProfile $computer
|
||||
if (-not $p.Valid) { return "Not Found" }
|
||||
|
||||
$fault = Get-DemoRand $p.Name 'netfault'
|
||||
if ($fault -lt 0.015) { return "Not Found" }
|
||||
if ($fault -lt 0.03) { return "DNS Issue" }
|
||||
|
||||
$stage = (Get-DemoStage $p).Name
|
||||
if ($p.IsDest) {
|
||||
if ($stage -eq 'NotStarted' -and (Get-DemoRand $p.Name 'staged') -lt 0.5) { return "Not Found" } # not imaged yet
|
||||
if ($stage -eq 'RestoreInProgress') { return "Online" }
|
||||
} else {
|
||||
if ($stage -eq 'BackupInProgress') { return "Online" }
|
||||
}
|
||||
|
||||
$odds = if ($p.Laptop) { 0.6 } else { 0.9 }
|
||||
if (-not $p.IsDest -and $stage -eq 'Completed') { $odds = 0.45 } # old hardware gets unplugged
|
||||
$bucket = [math]::Floor((Get-DemoNow) / 600)
|
||||
if ((Get-DemoRand $p.Name "ping$bucket") -lt $odds) { return "Online" }
|
||||
return "Timed Out"
|
||||
}
|
||||
|
||||
function Start-DemoLatency([int]$scale = 1) {
|
||||
$ms = [int]$script:DemoCfg.DelayMsPerDevice * $scale
|
||||
if ($ms -gt 0) { Start-Sleep -Milliseconds $ms }
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------------------
|
||||
# Provider interface
|
||||
# -----------------------------------------------------------------------------------------
|
||||
function Initialize-Provider {
|
||||
Write-Host "Demo provider loaded: simulated fleet (no SCCM, AD, DNS or SMB calls are made)." -ForegroundColor DarkMagenta
|
||||
}
|
||||
|
||||
function Open-ProviderCycle { return @{ PreviousLocation = Get-Location } }
|
||||
|
||||
function Close-ProviderCycle { param($Session) Set-Location $Session.PreviousLocation }
|
||||
|
||||
function pingDevice {
|
||||
param ([string]$computer)
|
||||
if ([string]::IsNullOrWhiteSpace($computer) -or $computer -eq "N/A") { return "Not Found" }
|
||||
if ($v) { Write-Host " > Fast Ping: $computer... " -ForegroundColor DarkGray -NoNewline }
|
||||
Start-DemoLatency
|
||||
|
||||
$state = Get-DemoNetworkState $computer
|
||||
switch ($state) {
|
||||
"Online" { if ($v) { Write-Host "Online" -ForegroundColor Green } }
|
||||
"Timed Out" { if ($v) { Write-Host "Timed Out." -ForegroundColor DarkYellow } }
|
||||
"DNS Issue" {
|
||||
if ($v) {
|
||||
$p = Get-DemoProfile $computer
|
||||
$other = Get-DemoPick $p.Name 'dnsother' @("PRVDT-", "PUBDT-", "PRVLT-")
|
||||
Write-Host "DNS Issue ($($p.IP) resolves back to $other$(New-DemoSerial "$($p.Name)dns"))." -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
default { if ($v) { Write-Host "Not Found (No IP)." -ForegroundColor Red } }
|
||||
}
|
||||
return $state
|
||||
}
|
||||
|
||||
function Resolve-DeviceIP {
|
||||
param([string]$computerName)
|
||||
$p = Get-DemoProfile $computerName
|
||||
if ($p.Valid) { return $p.IP }
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-DiskSpace {
|
||||
param([string]$computer)
|
||||
if ($v) { Write-Host " > Checking C: Drive Storage (SMB Method)... " -ForegroundColor DarkGray -NoNewline }
|
||||
$p = Get-DemoProfile $computer
|
||||
if (-not $p.Valid) {
|
||||
if ($v) { Write-Host "$($script:DemoAdminShare) Share Unreachable" -ForegroundColor Red }
|
||||
return @{ Total = "N/A"; Free = "N/A" }
|
||||
}
|
||||
# A small drift so free space isn't frozen between cycles
|
||||
$drift = 2 * (Get-DemoRand $p.Name "drift$([math]::Floor((Get-DemoNow) / 3600))") - 1
|
||||
$total = [math]::Round($p.DiskSize * 0.93, 1)
|
||||
$free = [math]::Round([math]::Max(1.2, $p.SrcFree + $drift), 1)
|
||||
if ($v) { Write-Host "${free}GB Free out of ${total}GB" -ForegroundColor Green }
|
||||
return @{ Total = "$total GB"; Free = "$free GB" }
|
||||
}
|
||||
|
||||
function Format-DemoLogLine([datetime]$at, [datetime]$started, [string]$message) {
|
||||
$inv = [System.Globalization.CultureInfo]::InvariantCulture
|
||||
$elapsed = $at - $started
|
||||
if ($elapsed.TotalSeconds -lt 0) { $elapsed = [timespan]::Zero }
|
||||
$off = [System.TimeZoneInfo]::Local.GetUtcOffset($at)
|
||||
$sign = if ($off.Ticks -lt 0) { "-" } else { "+" }
|
||||
$offStr = "{0}{1:D2}:{2:D2}" -f $sign, [math]::Abs($off.Hours), [math]::Abs($off.Minutes)
|
||||
return "{0}, {1} {2}, {3:D2}:{4:D2}:{5:D2}, {6}" -f $at.ToString("d MMM yyyy", $inv), $at.ToString("HH:mm:ss", $inv), $offStr, [int][math]::Floor($elapsed.TotalHours), $elapsed.Minutes, $elapsed.Seconds, $message
|
||||
}
|
||||
|
||||
function Get-LogInfo {
|
||||
param ([string]$computer, [string]$logName)
|
||||
|
||||
$result = New-LogInfoResult
|
||||
if ($v) { Write-Host " > Scraping logs for '$logName'..." -ForegroundColor DarkGray }
|
||||
|
||||
$p = Get-DemoProfile $computer
|
||||
$isScan = $logName -like "*scan*"
|
||||
$found = $null # @{ Folder; Raw; Modified }
|
||||
|
||||
if ($p.Valid) {
|
||||
$stage = Get-DemoStage $p
|
||||
$active = if ($isScan) { 'BackupInProgress' } else { 'RestoreInProgress' }
|
||||
$doneFrom = if ($isScan) { 0.40 } else { 0.70 }
|
||||
$now = Get-Date
|
||||
|
||||
if ($stage.Name -eq $active) {
|
||||
# Task sequence still running: log lives in SMSTSLog and is being written to
|
||||
$msg = if ($isScan -and $p.Failing -and $stage.Progress -gt 0.5) {
|
||||
'errorText, "Insufficient free space on the migration store"'
|
||||
} else {
|
||||
$pct = [int][math]::Floor($stage.Progress * 100)
|
||||
if ($pct -lt 8) { "PHASE, Initializing" }
|
||||
elseif ($pct -lt 20) { if ($isScan) { "PHASE, Scanning" } else { "PHASE, Gathering" } }
|
||||
elseif ($pct -lt 35) { "detectedUser, CORP\$($p.User), includedInMigration, Yes" }
|
||||
elseif ($pct -lt 50) { if ($isScan) { "PHASE, Collecting" } else { "PHASE, Applying" } }
|
||||
else { "totalPercentageCompleted, $pct" }
|
||||
}
|
||||
$modified = $now.AddSeconds(-1 * (Get-DemoInt $p.Name "lm$([math]::Floor((Get-DemoNow) / 60))" 20 240))
|
||||
$found = @{ Folder = $script:DemoLogFolders[0]; Raw = (Format-DemoLogLine $modified $stage.StageStarted $msg); Modified = $modified }
|
||||
} elseif ($stage.Phase -ge $doneFrom) {
|
||||
# Finished: the task sequence has ended and the log was moved to CCM\Logs
|
||||
$L = $script:DemoLifecycle
|
||||
$cycleStart = (Get-DemoNow) - ($stage.Phase * $L)
|
||||
$startFrac = if ($isScan) { 0.25 } else { 0.55 }
|
||||
$began = [DateTimeOffset]::FromUnixTimeSeconds([long]($cycleStart + $startFrac * $L)).LocalDateTime
|
||||
$ended = [DateTimeOffset]::FromUnixTimeSeconds([long]($cycleStart + $doneFrom * $L)).LocalDateTime
|
||||
$found = @{ Folder = $script:DemoLogFolders[1]; Raw = (Format-DemoLogLine $ended $began "Successful run"); Modified = $ended }
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($folderRel in $script:DemoLogFolders) {
|
||||
$folder = "\\$computer\$($script:DemoAdminShare)\$folderRel"
|
||||
if ($v) { Write-Host " Checking $folder... " -ForegroundColor DarkGray -NoNewline }
|
||||
if (-not $p.Valid) { if ($v) { Write-Host "Path doesn't exist." -ForegroundColor DarkGray }; continue }
|
||||
if ($found -and $found.Folder -eq $folderRel) {
|
||||
if ($v) { Write-Host "Found!" -ForegroundColor Green }
|
||||
$result.LastModified = $found.Modified
|
||||
Set-LogInfoLine $result $found.Raw
|
||||
$result.Found = $true
|
||||
$result.Path = "$folder\$logName.log"
|
||||
break
|
||||
}
|
||||
if ($v) { Write-Host "Empty/Not there." -ForegroundColor DarkGray }
|
||||
}
|
||||
return $result
|
||||
}
|
||||
|
||||
function Get-DeviceAssociations {
|
||||
param([string]$SourceComputer)
|
||||
Start-DemoLatency 2
|
||||
$p = Get-DemoProfile $SourceComputer
|
||||
if (-not $p.Valid -or $p.IsDest -or $p.NoAssoc) { return $null }
|
||||
|
||||
$status = switch ((Get-DemoStage $p).Name) {
|
||||
'NotStarted' { 0 }
|
||||
'BackupInProgress' { 0 }
|
||||
'BackupCompleted' { 1 }
|
||||
'RestoreInProgress' { 1 }
|
||||
default { 2 }
|
||||
}
|
||||
$dest = Get-DemoDestName $p.Name
|
||||
$assoc = [PSCustomObject]@{ SourceComputer = $p.Name; DestinationComputer = $dest; RestoreName = $dest; MigrationStatus = $status }
|
||||
$assoc | Add-Member -MemberType ScriptMethod -Name Get -Value { } # SMS_StateMigration lazy-property refresh; nothing to load here
|
||||
return @($assoc)
|
||||
}
|
||||
|
||||
function Get-DeviceInventory {
|
||||
param([string]$computerName, $Session)
|
||||
$p = Get-DemoProfile $computerName
|
||||
if (-not $p.Valid) { return @{} }
|
||||
|
||||
$vis = if ($p.Public) { "Public" } else { "Private" }
|
||||
$form = if ($p.Laptop) { "Laptops" } else { "Desktops" }
|
||||
$floor = $p.Location.Substring(0, 2)
|
||||
|
||||
$groups = [System.Collections.Generic.List[string]]::new()
|
||||
$groups.Add("$vis PCs")
|
||||
if ($p.Laptop) { $groups.Add("Wireless_Endpoints"); $groups.Add("VPN-Users") }
|
||||
$groups.Add("PRN-$($p.Building.Code)-$floor-Copier")
|
||||
if ((Get-DemoRand $p.Name 'grpbl') -lt 0.8) { $groups.Add("BitLocker-Enforced") }
|
||||
if ($p.IsDest) { $groups.Add("Win11-Deployment") } else { $groups.Add("Win10-Legacy") }
|
||||
if ($p.Public) { $groups.Add("Kiosk-AutoLogon") }
|
||||
|
||||
$primary = if ($p.Public) { "multi-user" } else { $p.User }
|
||||
$lastUser = if ($p.Public) { "$($p.Name)-U" } else { $p.User }
|
||||
$uda = if ($p.Public) { "$($p.Name.ToLower())-u" } else { $p.User }
|
||||
$updated = (Get-Date).AddDays(-1 * (Get-DemoInt $p.Source 'upd' 20 1400))
|
||||
if ($p.IsDest) { $updated = (Get-Date).AddDays(-1 * (Get-DemoInt $p.Name 'upd' 1 30)) }
|
||||
|
||||
return @{
|
||||
OU = "Company Devices/$vis/$form"
|
||||
Groups = $groups -join "<br>"
|
||||
MAC = $p.Mac
|
||||
Model = $p.Model
|
||||
RAM = $p.RAM
|
||||
InventoryIP = $p.IP
|
||||
LastUser = $lastUser
|
||||
ClientStatus = if ($p.Inactive) { "Installed / Inactive" } else { "Installed / Active" }
|
||||
Updated = $updated.ToString("M/d/yyyy")
|
||||
Location = $p.Location
|
||||
Vanity = $p.Vanity
|
||||
Department = $p.Department.Name
|
||||
Building = $p.Building.Name
|
||||
Mount = $p.Mount
|
||||
PrimaryUser = $primary
|
||||
PhoneExt = $p.PhoneExt
|
||||
InfoBy = $p.InfoBy
|
||||
Notes = $p.Notes
|
||||
RoomType = $p.RoomType
|
||||
SCCMPrimaryUsers = $uda
|
||||
Disk = "Size: $($p.DiskSize) GB<br>Model: $($p.DiskModel)<br>S/N: $($p.DiskSerial)."
|
||||
}
|
||||
}
|
||||
|
||||
function Get-InstalledSoftware {
|
||||
param([string]$ComputerName)
|
||||
$p = Get-DemoProfile $ComputerName
|
||||
if (-not $p.Valid) { return @() }
|
||||
|
||||
$rows = [System.Collections.Generic.List[object]]::new()
|
||||
$baseDays = if ($p.IsDest) { Get-DemoInt $p.Name 'img' 10 60 } else { Get-DemoInt $p.Name 'img' 900 2200 }
|
||||
$imaged = (Get-Date).Date.AddDays(-$baseDays)
|
||||
$add = {
|
||||
param($name, $ver, $pub, $date, $src)
|
||||
$rows.Add([PSCustomObject]@{ Name = $name; Version = $ver; Publisher = $pub; Source = $src; InstallDate = $date })
|
||||
}
|
||||
|
||||
foreach ($b in $script:DemoBaseline) {
|
||||
$ver = if ($p.IsDest) { $b[2] } else { $b[1] }
|
||||
& $add $b[0] $ver $b[3] $imaged ""
|
||||
}
|
||||
$tools = $script:DemoModels[$p.Vendor]
|
||||
& $add $tools.Tool "9.$(Get-DemoInt $p.Name 'toolv' 1 9).0" $tools.ToolPub $imaged ""
|
||||
if ($p.Laptop) { & $add "Cisco Secure Client - AnyConnect VPN" "5.1.4.74" "Cisco Systems, Inc." $imaged "" }
|
||||
if ($p.Public) {
|
||||
& $add "Kiosk Launcher" "3.2.1" "Company IT" $imaged "\\fileserver\software\KioskLauncher\"
|
||||
& $add "Shared PC Cleanup Utility" "1.4.0" "Company IT" $imaged "\\fileserver\software\SharedPC\"
|
||||
}
|
||||
|
||||
# Specialty apps: the source has all of its department's picks; the replacement only gets
|
||||
# the ones that have been reinstalled so far (the "Missing Software" page shows the gap).
|
||||
$stage = Get-DemoStage $p
|
||||
foreach ($app in $p.Apps) {
|
||||
if ($p.IsDest) {
|
||||
$odds = switch ($stage.Name) { 'Completed' { 0.85 } 'RestoreInProgress' { 0.5 } 'BackupCompleted' { 0.2 } default { 0.0 } }
|
||||
if ((Get-DemoRand $p.Name "has$app") -ge $odds) { continue }
|
||||
}
|
||||
$installed = $imaged.AddDays((Get-DemoInt $p.Name "inst$app" 1 ([math]::Max(2, $baseDays - 1))))
|
||||
foreach ($entry in $script:DemoApps[$app]) {
|
||||
& $add $entry[0] $entry[1] $entry[2] $installed "\\fileserver\software\$($app)\"
|
||||
}
|
||||
}
|
||||
return $rows
|
||||
}
|
||||
Reference in new issue
Block a user