# ========================================================================================= # 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:DemoMonitors = @("HP E24 G5", "HP E27 G5", "HP P24h G5", "DELL P2422H", "DELL P2723DE", "LEN T24i-30", "LEN T27h-30", "LG 27UK850") $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() } # ----------------------------------------------------------------------------------------- # Date-batch tags ("M/d") sit on fixed working-day offsets from today. The seed records the # labels it used in Demo-Dates.json and the worker moves them along when the day changes, so # a demo container that runs for weeks still shows this week's deployments. # ----------------------------------------------------------------------------------------- $script:DemoDateOffsets = @(-4, -3, -2, -1, 0, 1) function Get-DemoWorkday([datetime]$from, [int]$offset) { $d = $from.Date; $step = [math]::Sign($offset); $left = [math]::Abs($offset) while ($left -gt 0) { $d = $d.AddDays($step); if ($d.DayOfWeek -ne 'Saturday' -and $d.DayOfWeek -ne 'Sunday') { $left-- } } return $d } function Get-DemoDateTags { $today = Get-Date; return @($script:DemoDateOffsets | ForEach-Object { (Get-DemoWorkday $today $_).ToString("M/d") }) } function Update-DemoDateTags([string]$DataDir) { $metaPath = Join-Path $DataDir "Demo-Dates.json" $trackedPath = Join-Path $DataDir "Tracked-Devices.json" if (-not (Test-Path $metaPath) -or -not (Test-Path $trackedPath)) { return } try { $old = @(([System.IO.File]::ReadAllText($metaPath) | ConvertFrom-Json).Tags) } catch { return } $new = Get-DemoDateTags if ($old.Count -ne $new.Count -or ($old -join '|') -eq ($new -join '|')) { return } $rename = @{} for ($i = 0; $i -lt $old.Count; $i++) { $rename[$old[$i]] = $new[$i] } try { $tracked = [System.IO.File]::ReadAllText($trackedPath) | ConvertFrom-Json } catch { return } $out = [ordered]@{} foreach ($prop in $tracked.PSObject.Properties) { $name = if ($rename.ContainsKey($prop.Name)) { $rename[$prop.Name] } else { $prop.Name } $out[$name] = $prop.Value } $tmp = "$trackedPath.tmp" [System.IO.File]::WriteAllText($tmp, ($out | ConvertTo-Json -Depth 5), [System.Text.Encoding]::UTF8) Move-Item -LiteralPath $tmp -Destination $trackedPath -Force [System.IO.File]::WriteAllText($metaPath, (@{ Tags = $new } | ConvertTo-Json), [System.Text.Encoding]::UTF8) Write-Host "Demo: date tags moved to today ($($old -join ', ') -> $($new -join ', '))" -ForegroundColor DarkCyan } # ----------------------------------------------------------------------------------------- # 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]) } # A few old devices have been unplugged / powered off for days (not while they're mid-migration) $p.OfflineDays = if ((Get-DemoRand $s 'longoff') -lt 0.07) { Get-DemoInt $s 'offdays' 1 21 } else { 0 } # Monitors at the desk move to the replacement, so both share the source's set $monCount = if ($p.Laptop) { Get-DemoPick $s 'mons' @(0, 1, 1, 2) } else { Get-DemoPick $s 'mons' @(1, 2, 2, 2, 3) } $p.Monitors = @(1..3 | Select-Object -First $monCount | ForEach-Object { Get-DemoPick $s "mon$(if ((Get-DemoRand $s 'monmatch') -lt 0.7) { 1 } else { $_ })" $script:DemoMonitors }) # 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 $p.NoSccm = (-not $p.IsDest) -and (Get-DemoRand $n 'nosccm') -lt 0.025 # broken client, never reported $p.NoAd = (-not $p.IsDest) -and (Get-DemoRand $n 'noad') -lt 0.012 # computer object deleted } $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 (-not $p.IsDest -and $p.OfflineDays -gt 0 -and $stage -in @('NotStarted', 'Completed')) { return "Timed Out" } if ($p.IsDest) { if (Test-DemoNotImaged $p) { return "Not Found" } # not imaged yet: no DNS record 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" } # A replacement that hasn't been imaged yet isn't in DNS, AD or SCCM function Test-DemoNotImaged([hashtable]$p) { return $p.IsDest -and (Get-DemoStage $p).Name -eq 'NotStarted' -and (Get-DemoRand $p.Name 'staged') -lt 0.5 } 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 @{ InAD = $false; InSCCM = $false } } # a typo: nothing knows it if (Test-DemoNotImaged $p) { return @{ InAD = $false; InSCCM = $false } } $flags = @{ InAD = -not $p.NoAd; InSCCM = -not $p.NoSccm } $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)) } $monitors = @($p.Monitors) $inv = @{ InAD = $true InSCCM = $true MonitorCount = "$($monitors.Count)" Monitors = if ($monitors.Count) { $monitors -join "
" } else { "None" } OU = "Company Devices/$vis/$form" Groups = $groups -join "
" 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
Model: $($p.DiskModel)
S/N: $($p.DiskSerial)." } # What a missing AD object / SCCM record would take away if (-not $flags.InAD) { $inv.InAD = $false; foreach ($k in @("OU", "Groups")) { $inv.Remove($k) } } if (-not $flags.InSCCM) { $inv.InSCCM = $false foreach ($k in @("MAC", "Model", "RAM", "InventoryIP", "LastUser", "ClientStatus", "Updated", "Location", "Vanity", "Department", "Building", "Mount", "PrimaryUser", "PhoneExt", "InfoBy", "Notes", "RoomType", "SCCMPrimaryUsers", "Disk", "MonitorCount", "Monitors")) { $inv.Remove($k) } } return $inv } # Client's last activity, used the first time a device is tracked and doesn't answer function Get-DeviceLastSeen { param([string]$computerName, $Session) $p = Get-DemoProfile $computerName if (-not $p.Valid -or (Test-DemoNotImaged $p) -or $p.NoSccm) { return $null } if (-not $p.IsDest -and $p.OfflineDays -gt 0) { return (Get-Date).AddDays(-$p.OfflineDays).AddHours(-(Get-DemoInt $p.Name 'offhrs' 0 9)) } return (Get-Date).AddMinutes(-(Get-DemoInt $p.Name "seen$([math]::Floor((Get-DemoNow) / 600))" 5 55)) } 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 }