# ========================================================================================= # 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. Its migration happens on the # day of its date tag (10/14 and so on), so the unmodified worker loop sees that day's devices # go Not Started -> Backup In Progress -> Backup Completed -> Restore In Progress -> Completed, # earlier days finished (apart from a few with a real reason, such as a broken client or low # disk) and later days not started yet. A simulated team keeps the date tags moving: see # Invoke-DemoTeam at the end of this file. # # 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:DemoTeamCfg = $script:DemoCfg.Team $script:DemoDataDir = Join-Path (Split-Path $PSScriptRoot -Parent) "Data" $script:DemoProfiles = @{} $script:DemoAlphabet = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ" $script:DemoAdminShare = 'C$' $script:DemoLogFolders = @('Windows\CCM\Logs\SMSTSLog', 'Windows\CCM\Logs') $script:DemoStageOrder = @('NotStarted', 'BackupInProgress', 'BackupCompleted', 'RestoreInProgress', 'Completed') # --- 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() } # ----------------------------------------------------------------------------------------- # 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.NoAssoc = (Get-DemoRand $s 'assoc') -lt 0.08 # Problems that stop a migration on its day (until the team fixes them) $p.LowDisk = (Get-DemoRand $s 'lowdisk') -lt 0.04 $p.BrokenWmi = (Get-DemoRand $s 'wmi') -lt 0.02 $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 = 30.0 + 150 * (Get-DemoRand $s 'free') $p.LowFree = 3.0 + 9 * (Get-DemoRand $s 'freeL') $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 $p.OfflineDays = if ((Get-DemoRand $s 'longoff') -lt 0.03) { 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.02 # 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 } # ----------------------------------------------------------------------------------------- # Schedule: each source device migrates on the day of its date tag # ----------------------------------------------------------------------------------------- function Test-DemoWorkday([datetime]$d) { return @($script:DemoTeamCfg.Weekdays) -contains [int]$d.DayOfWeek } function Get-DemoWorkday([datetime]$from, [int]$offset) { # $offset working days from $from (0 = $from itself if it's a working day, else the next one) $d = $from.Date if ($offset -ge 0) { while (-not (Test-DemoWorkday $d)) { $d = $d.AddDays(1) } for ($i = 0; $i -lt $offset; $i++) { do { $d = $d.AddDays(1) } until (Test-DemoWorkday $d) } } else { for ($i = 0; $i -lt -$offset; $i++) { do { $d = $d.AddDays(-1) } until (Test-DemoWorkday $d) } } return $d } function ConvertFrom-DemoDateTag([string]$tag) { if ($tag -notmatch '^(\d{1,2})/(\d{1,2})$') { return $null } $today = (Get-Date).Date try { $d = [datetime]::new($today.Year, [int]$Matches[1], [int]$Matches[2]) } catch { return $null } if (($d - $today).TotalDays -gt 183) { $d = $d.AddYears(-1) } elseif (($today - $d).TotalDays -gt 183) { $d = $d.AddYears(1) } return $d } function ConvertTo-DemoDateTag([datetime]$d) { return $d.ToString("M/d", [System.Globalization.CultureInfo]::InvariantCulture) } # Tags as a list of @{ Name; Devices; Skip } from a parsed Tracked-Devices.json (object or hashtable) function Get-DemoTagList($tracked) { $out = [System.Collections.Generic.List[object]]::new() if (-not $tracked) { return $out } $pairs = if ($tracked -is [System.Collections.IDictionary]) { $tracked.GetEnumerator() | ForEach-Object { @{ Name = $_.Key; Value = $_.Value } } } else { $tracked.PSObject.Properties | ForEach-Object { @{ Name = $_.Name; Value = $_.Value } } } foreach ($pair in $pairs) { $v = $pair.Value $devs = if ($v -is [System.Collections.IDictionary]) { $v.devices } elseif ($v.PSObject.Properties.Name -contains 'devices') { $v.devices } else { $v } $skip = if ($v -is [System.Collections.IDictionary]) { [bool]$v.skipUSMT } elseif ($v.PSObject.Properties.Name -contains 'skipUSMT') { [bool]$v.skipUSMT } else { $false } $out.Add(@{ Name = $pair.Name; Devices = @($devs | Where-Object { $_ } | ForEach-Object { "$_".Trim().ToUpper() }); Skip = $skip }) } return $out } $script:DemoSchedule = $null # source -> scheduled day $script:DemoFixed = [System.Collections.Generic.HashSet[string]]::new() # problems the team has fixed $script:DemoPlans = @{} $script:DemoScheduleStamp = '' $script:DemoScheduleChecked = [datetime]::MinValue # -Pin: use this roster as is (the seed and the team work on one in memory), don't re-read the file function Set-DemoSchedule($tracked, $fixed, [switch]$Pin) { $sched = @{} foreach ($t in (Get-DemoTagList $tracked)) { $day = ConvertFrom-DemoDateTag $t.Name if (-not $day) { continue } foreach ($d in $t.Devices) { if ($d -notmatch ',' -and (-not $sched.ContainsKey($d) -or $sched[$d] -lt $day)) { $sched[$d] = $day } } } $script:DemoSchedule = $sched $script:DemoFixed = [System.Collections.Generic.HashSet[string]]::new([string[]]@($fixed | Where-Object { $_ })) $script:DemoPlans = @{} $script:DemoSchedulePinned = [bool]$Pin } # Re-reads the roster (and the team's fixes) when they change on disk; checked at most every few seconds $script:DemoSchedulePinned = $false function Import-DemoSchedule { if ($script:DemoSchedulePinned) { return } if ($script:DemoSchedule -and ((Get-Date) - $script:DemoScheduleChecked).TotalSeconds -lt 3) { return } $script:DemoScheduleChecked = Get-Date $tp = Join-Path $script:DemoDataDir "Tracked-Devices.json" $fp = Join-Path $script:DemoDataDir "Demo-Team.json" $stamp = (@($tp, $fp) | ForEach-Object { if (Test-Path $_) { (Get-Item $_).LastWriteTimeUtc.Ticks } else { 0 } }) -join '|' if ($script:DemoSchedule -and $stamp -eq $script:DemoScheduleStamp) { return } $tracked = $null; $fixed = @() try { $tracked = [System.IO.File]::ReadAllText($tp) | ConvertFrom-Json } catch {} try { $fixed = @(([System.IO.File]::ReadAllText($fp) | ConvertFrom-Json).Fixed) } catch {} Set-DemoSchedule $tracked $fixed $script:DemoScheduleStamp = $stamp } # What the problems that block a device are, before the team fixes them function Test-DemoOffline([hashtable]$sp) { return $sp.OfflineDays -gt 0 -and -not $script:DemoFixed.Contains($sp.Name) } # The day's timeline for a source: when backup and restore run and whether something stops it function Get-DemoPlan([string]$source) { Import-DemoSchedule $day = $script:DemoSchedule[$source] if (-not $day) { return $null } $d = $day.ToString('yyyyMMdd') $key = "$source|$d" if ($script:DemoPlans.ContainsKey($key)) { return $script:DemoPlans[$key] } $sp = Get-DemoProfile $source $ws = [timespan]$script:DemoTeamCfg.WorkStart; $we = [timespan]$script:DemoTeamCfg.WorkEnd $room = [math]::Max(30, ($we - $ws).TotalMinutes - 150) # last ones still finish around closing time $bStart = $day.Add($ws).AddMinutes([math]::Floor((Get-DemoRand $source "start$d") * $room)) $bEnd = $bStart.AddMinutes((Get-DemoInt $source "bdur$d" 30 70)) $rStart = $bEnd.AddMinutes((Get-DemoInt $source "swap$d" 15 60)) # hardware swap at the desk $rEnd = $rStart.AddMinutes((Get-DemoInt $source "rdur$d" 20 45)) $fail = $null if (-not $script:DemoFixed.Contains($source)) { $r = Get-DemoRand $source "err$d" if ($sp.NoSccm) { $fail = 'BadClient' } elseif (Test-DemoOffline $sp) { $fail = 'Offline' } elseif ($sp.BrokenWmi) { $fail = 'WMI' } elseif ($sp.LowDisk) { $fail = 'LowDisk' } elseif ($r -lt 0.025) { $fail = 'BackupError' } elseif ($r -lt 0.035) { $fail = 'RestoreError' } } $failAt = switch ($fail) { { $_ -in 'LowDisk', 'BackupError' } { $bStart.AddMinutes(0.55 * ($bEnd - $bStart).TotalMinutes); break } 'RestoreError' { $rStart.AddMinutes(0.6 * ($rEnd - $rStart).TotalMinutes); break } $null { $null; break } default { $bStart } # can't even start: the task sequence never runs } $plan = @{ Day = $day; BackupStart = $bStart; BackupEnd = $bEnd; RestoreStart = $rStart; RestoreEnd = $rEnd; Fail = $fail; FailAt = $failAt } $script:DemoPlans[$key] = $plan return $plan } # Where a migration is right now. Both halves of a pair share the source's plan. function Get-DemoStage([hashtable]$p) { $plan = Get-DemoPlan $p.Source $now = Get-Date $st = { param($name, $progress, $from, $to, $failed) @{ Name = $name; Progress = $progress; StageStarted = $from; StageEnded = $to; Plan = $plan; Failed = [bool]$failed } } if (-not $plan) { return & $st 'NotStarted' 0 $now.Date $now.Date $false } # not scheduled if ($plan.Fail -in 'BadClient', 'Offline', 'WMI') { return & $st 'NotStarted' 0 $plan.Day $plan.Day $false } if ($now -lt $plan.BackupStart) { return & $st 'NotStarted' 0 $plan.Day $plan.BackupStart $false } $backupFails = $plan.Fail -in 'LowDisk', 'BackupError' if ($now -lt $plan.BackupEnd -or $backupFails) { if ($backupFails -and $now -ge $plan.FailAt) { return & $st 'BackupInProgress' 0.55 $plan.BackupStart $plan.BackupEnd $true } return & $st 'BackupInProgress' (($now - $plan.BackupStart).TotalSeconds / ($plan.BackupEnd - $plan.BackupStart).TotalSeconds) $plan.BackupStart $plan.BackupEnd $false } if ($now -lt $plan.RestoreStart) { return & $st 'BackupCompleted' 0 $plan.BackupEnd $plan.RestoreStart $false } if ($now -lt $plan.RestoreEnd -or $plan.Fail -eq 'RestoreError') { if ($plan.Fail -eq 'RestoreError' -and $now -ge $plan.FailAt) { return & $st 'RestoreInProgress' 0.6 $plan.RestoreStart $plan.RestoreEnd $true } return & $st 'RestoreInProgress' (($now - $plan.RestoreStart).TotalSeconds / ($plan.RestoreEnd - $plan.RestoreStart).TotalSeconds) $plan.RestoreStart $plan.RestoreEnd $false } return & $st 'Completed' 1 $plan.RestoreEnd $plan.RestoreEnd $false } 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 (Test-DemoOffline $p)) { 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) { if (-not $p.IsDest) { return $false } $stage = Get-DemoStage $p if ($stage.Name -ne 'NotStarted') { return $false } # Replacements are imaged a few days ahead of their date if (-not $stage.Plan -or ($stage.Plan.Day - (Get-Date).Date).TotalDays -gt 3) { return $true } return (Get-DemoRand $p.Name 'staged') -lt 0.2 } 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) $sp = Get-DemoProfile $p.Source $base = if ($sp.LowDisk -and -not $script:DemoFixed.Contains($sp.Name)) { $sp.LowFree } else { $p.SrcFree } $free = [math]::Round([math]::Max(1.2, $base + $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 $plan = $stage.Plan $active = if ($isScan) { 'BackupInProgress' } else { 'RestoreInProgress' } $doneFrom = if ($isScan) { 2 } else { 4 } # index in DemoStageOrder $now = Get-Date if ($stage.Name -eq $active) { # Task sequence still running (or stopped on an error): log lives in SMSTSLog if ($stage.Failed) { $err = switch ($plan.Fail) { 'LowDisk' { 'Insufficient free space on the migration store' } 'BackupError' { Get-DemoPick $p.Source 'berr' @('Failed to gather data: Access is denied. (0x80070005)', 'Return code 61: Processing stopped due to an I/O error', 'Return code 27: Invalid store path') } default { Get-DemoPick $p.Source 'rerr' @('Failed to apply settings: the profile is in use (0x80070020)', 'Return code 71: Unable to start. Run USMT with elevated privileges') } } $msg = "errorText, `"$err`"" $modified = $plan.FailAt } else { $pct = [int][math]::Floor($stage.Progress * 100) $msg = 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)) if ($modified -lt $stage.StageStarted) { $modified = $stage.StageStarted } } $found = @{ Folder = $script:DemoLogFolders[0]; Raw = (Format-DemoLogLine $modified $stage.StageStarted $msg); Modified = $modified } } elseif ($plan -and $script:DemoStageOrder.IndexOf($stage.Name) -ge $doneFrom) { # Finished: the task sequence has ended and the log was moved to CCM\Logs $began = if ($isScan) { $plan.BackupStart } else { $plan.RestoreStart } $ended = if ($isScan) { $plan.BackupEnd } else { $plan.RestoreEnd } $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) { return $null } # Some associations are only created in SCCM a couple of days before the device's date if ($p.NoAssoc) { $plan = Get-DemoPlan $p.Name if (-not $plan -or ($plan.Day - (Get-Date).Date).TotalDays -gt 2) { 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 } } $fixed = $script:DemoFixed.Contains($p.Name) $flags = @{ InAD = -not $p.NoAd; InSCCM = -not ($p.NoSccm -and -not $fixed) } $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 -or ($p.BrokenWmi -and -not $fixed)) { "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 -and -not $script:DemoFixed.Contains($p.Name))) { return $null } if (-not $p.IsDest -and (Test-DemoOffline $p)) { 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 } # ========================================================================================= # Simulated team (demo worker only) # # Plays the IT team that owns the roster: the next two weeks of working days are tagged, # and during working hours someone adds devices, reschedules or drops one, tags a stuck PC # with its problem and later fixes it and moves it to a new day. Each morning finished days # are closed out (completed devices untracked, stragglers rescheduled) and a new day is # scheduled at the end. Every action is printed to the worker log. # ========================================================================================= $script:DemoTechs = @("MR", "SK", "TB", "JC", "AL") $script:DemoIssueTags = @{ BadClient = "Bad Client"; WMI = "Broken WMI"; LowDisk = "Low Disk Space"; BackupError = "USMT Errors"; RestoreError = "USMT Errors"; Offline = "Can't Reach" } $script:DemoDeptTags = @{ Accounting = "Accounting"; Sales = "Sales Floor"; Executive = "Exec Suite" } # Roster as an ordered table of tag -> @{ devices = List[string]; skipUSMT = bool } function ConvertTo-DemoRoster($tracked) { $r = [ordered]@{} foreach ($t in (Get-DemoTagList $tracked)) { $r[$t.Name] = @{ devices = [System.Collections.Generic.List[string]]::new([string[]]$t.Devices); skipUSMT = $t.Skip } } return $r } function Save-DemoRoster([string]$path, $roster) { # Date tags first, in date order, then the rest as they were $dated = @($roster.Keys | Where-Object { ConvertFrom-DemoDateTag $_ } | Sort-Object { ConvertFrom-DemoDateTag $_ }) $out = [ordered]@{} foreach ($k in $dated + @($roster.Keys | Where-Object { $dated -notcontains $_ })) { if ($roster[$k].devices.Count -eq 0) { continue } # an empty tag is deleted $out[$k] = [ordered]@{ devices = @($roster[$k].devices); skipUSMT = [bool]$roster[$k].skipUSMT } } $tmp = "$path.tmp" [System.IO.File]::WriteAllText($tmp, ($out | ConvertTo-Json -Depth 5), [System.Text.Encoding]::UTF8) Move-Item -LiteralPath $tmp -Destination $path -Force } function Get-DemoNewDevice($roster, $state) { $all = [System.Collections.Generic.HashSet[string]]::new() foreach ($t in $roster.Values) { foreach ($d in $t.devices) { foreach ($n in ($d -split '\s*,\s*')) { $null = $all.Add($n) } } } do { $state.Serial = [int]$state.Serial + 1 $seed = "team$($state.Serial)" $r = Get-DemoRand $seed 'prefix' $prefix = if ($r -lt 0.55) { "PRVDT" } elseif ($r -lt 0.80) { "PUBDT" } elseif ($r -lt 0.95) { "PRVLT" } else { "PUBLT" } $name = "$prefix-$(New-DemoSerial $seed)" } while ($all.Contains($name)) return $name } function Remove-DemoFromRoster($roster, [string]$dev, [string[]]$only) { foreach ($k in @($roster.Keys)) { if (-not $only -or $only -contains $k) { $null = $roster[$k].devices.Remove($dev) } } } function Add-DemoToTag($roster, [string]$tag, [string]$dev) { if (-not $roster.Contains($tag)) { $roster[$tag] = @{ devices = [System.Collections.Generic.List[string]]::new(); skipUSMT = $false } } if (-not $roster[$tag].devices.Contains($dev)) { $roster[$tag].devices.Add($dev) } } function Get-DemoDateTagsOf($roster, [string]$dev) { return @($roster.Keys | Where-Object { (ConvertFrom-DemoDateTag $_) -and $roster[$_].devices.Contains($dev) }) } # A stuck device: its problem has shown up by now function Get-DemoStuck([string]$dev) { $plan = Get-DemoPlan $dev if (-not $plan -or -not $plan.Fail -or (Get-Date) -lt $plan.FailAt) { return $null } return $plan.Fail } function Move-DemoDevice($roster, $state, [string]$dev, [datetime]$to) { $from = Get-DemoDateTagsOf $roster $dev Remove-DemoFromRoster $roster $dev $from Add-DemoToTag $roster (ConvertTo-DemoDateTag $to) $dev return ($from -join ", ") } # The days the team keeps tagged: the last few working days (still being cleaned up) and the # next two weeks of working days, today included function Get-DemoWindow { $today = (Get-Date).Date $kept = @(1..[int]$script:DemoTeamCfg.DaysKept | ForEach-Object { Get-DemoWorkday $today (-$_) }) $ahead = @(0..([int]$script:DemoTeamCfg.DaysAhead - 1) | ForEach-Object { Get-DemoWorkday $today $_ }) return @{ Oldest = ($kept | Sort-Object | Select-Object -First 1); Ahead = $ahead } } function Invoke-DemoRollover($roster, $state, $log) { $w = Get-DemoWindow $tech = Get-Random -InputObject $script:DemoTechs foreach ($tag in @($roster.Keys)) { $day = ConvertFrom-DemoDateTag $tag if (-not $day -or $day -ge $w.Oldest) { continue } $done = 0; $moved = [System.Collections.Generic.List[string]]::new(); $retired = 0 foreach ($dev in @($roster[$tag].devices)) { if ((Get-DemoStage (Get-DemoProfile $dev)).Name -eq 'Completed' -or -not (Get-DemoPlan $dev)) { Remove-DemoFromRoster $roster $dev; $done++; continue } $moves = [int]$state.Moves[$dev] if ($moves -ge 2) { Remove-DemoFromRoster $roster $dev; $retired++; continue } $state.Moves[$dev] = $moves + 1 $to = $w.Ahead[[math]::Min($w.Ahead.Count - 1, (Get-Random -Minimum 1 -Maximum 4))] $null = Move-DemoDevice $roster $state $dev $to $moved.Add("$dev -> $(ConvertTo-DemoDateTag $to)") } $roster.Remove($tag) if ($done + $moved.Count + $retired -eq 0) { continue } $msg = "[Team] $tech closed out $tag`: $done completed device(s) untracked" if ($moved.Count) { $msg += ", rescheduled $($moved -join ', ')" } if ($retired) { $msg += ", $retired dropped after two reschedules" } $log.Add($msg) } # Fill any working day in the next two weeks that has nothing scheduled $per = [int]$script:DemoTeamCfg.DevicesPerDay foreach ($day in $w.Ahead) { $tag = ConvertTo-DemoDateTag $day if ($roster.Contains($tag) -and $roster[$tag].devices.Count) { continue } $n = [math]::Max(3, $per + (Get-Random -Minimum -3 -Maximum 4)) $new = @(1..$n | ForEach-Object { Get-DemoNewDevice $roster $state }) foreach ($dev in $new) { Add-DemoToTag $roster $tag $dev $dept = (Get-DemoProfile $dev).Department.Name if ($script:DemoDeptTags.ContainsKey($dept) -and (Get-Random -Maximum 1.0) -lt 0.6) { Add-DemoToTag $roster $script:DemoDeptTags[$dept] $dev } } $log.Add("[Team] $tech scheduled $n devices for $tag") } } function Invoke-DemoTeamAction($roster, $state, $log) { $w = Get-DemoWindow $today = (Get-Date).Date $tech = Get-Random -InputObject $script:DemoTechs $upcoming = @($w.Ahead | Where-Object { $_ -gt $today }) $allDated = @($roster.Keys | Where-Object { ConvertFrom-DemoDateTag $_ }) $devsOn = { param($days) @($allDated | Where-Object { (ConvertFrom-DemoDateTag $_) -in $days } | ForEach-Object { $roster[$_].devices } | Select-Object -Unique) } $issueNames = @($script:DemoIssueTags.Values | Select-Object -Unique) $actions = @('label', 'label', 'fix', 'fix', 'add', 'add', 'move', 'move', 'drop', 'dept') | Sort-Object { Get-Random } foreach ($a in $actions) { switch ($a) { 'label' { # Someone notices a stuck PC and tags it with its problem $cands = @(& $devsOn @($allDated | ForEach-Object { ConvertFrom-DemoDateTag $_ } | Where-Object { $_ -le $today }) | Where-Object { $k = Get-DemoStuck $_; $k -and -not ($roster.Contains($script:DemoIssueTags[$k]) -and $roster[$script:DemoIssueTags[$k]].devices.Contains($_)) }) if (-not $cands.Count) { continue } $dev = Get-Random -InputObject $cands $tag = $script:DemoIssueTags[(Get-DemoStuck $dev)] Add-DemoToTag $roster $tag $dev $log.Add("[Team] $tech tagged $dev as '$tag'"); return } 'fix' { # A problem gets fixed: the PC goes back on the schedule $cands = @($issueNames | Where-Object { $roster.Contains($_) } | ForEach-Object { $roster[$_].devices } | Select-Object -Unique) if (-not $cands.Count) { continue } $dev = Get-Random -InputObject $cands $kind = (Get-DemoPlan $dev).Fail $what = switch ($kind) { 'BadClient' { "repaired the ConfigMgr client on" } 'WMI' { "rebuilt the WMI repository on" } 'LowDisk' { "cleared space on C: of" } 'Offline' { "got the user to power on" } default { "cleared the USMT error on" } } $null = $state.Fixed.Add($dev) $script:DemoFixed.Add($dev) | Out-Null Remove-DemoFromRoster $roster $dev $issueNames $to = $w.Ahead[[math]::Min($w.Ahead.Count - 1, (Get-Random -Minimum 1 -Maximum 4))] $from = Move-DemoDevice $roster $state $dev $to $log.Add("[Team] $tech $what $dev; moved it from $from to $(ConvertTo-DemoDateTag $to)"); return } 'add' { if (-not $upcoming.Count) { continue } $day = Get-Random -InputObject $upcoming $tag = ConvertTo-DemoDateTag $day $new = @(1..(Get-Random -Minimum 1 -Maximum 4) | ForEach-Object { Get-DemoNewDevice $roster $state }) foreach ($dev in $new) { Add-DemoToTag $roster $tag $dev } $log.Add("[Team] $tech added $($new -join ', ') to $tag"); return } 'move' { $cands = @(& $devsOn $upcoming | Where-Object { (Get-DemoStage (Get-DemoProfile $_)).Name -eq 'NotStarted' }) if (-not $cands.Count -or $upcoming.Count -lt 2) { continue } $dev = Get-Random -InputObject $cands $cur = Get-DemoScheduleDate $dev $to = Get-Random -InputObject @($upcoming | Where-Object { $_ -ne $cur }) $why = if ($to -lt $cur) { Get-Random -InputObject @("user asked to go earlier", "a slot opened up", "swapped with another desk") } else { Get-Random -InputObject @("user is out that day", "department asked for a later date", "waiting on a docking station", "swapped with another desk") } $from = Move-DemoDevice $roster $state $dev $to $log.Add("[Team] $tech moved $dev from $from to $(ConvertTo-DemoDateTag $to) ($why)"); return } 'drop' { $cands = @(& $devsOn $upcoming | Where-Object { (Get-DemoStage (Get-DemoProfile $_)).Name -eq 'NotStarted' }) if (-not $cands.Count) { continue } $dev = Get-Random -InputObject $cands $why = Get-Random -InputObject @("device is being retired", "already replaced last month", "user left the company", "duplicate entry") Remove-DemoFromRoster $roster $dev $log.Add("[Team] $tech removed $dev ($why)"); return } 'dept' { $cands = @(& $devsOn $upcoming | Where-Object { $dep = (Get-DemoProfile $_).Department.Name $script:DemoDeptTags.ContainsKey($dep) -and -not ($roster.Contains($script:DemoDeptTags[$dep]) -and $roster[$script:DemoDeptTags[$dep]].devices.Contains($_)) }) if (-not $cands.Count) { continue } $dev = Get-Random -InputObject $cands $tag = $script:DemoDeptTags[(Get-DemoProfile $dev).Department.Name] Add-DemoToTag $roster $tag $dev $log.Add("[Team] $tech added $dev to '$tag'"); return } } } } function Get-DemoScheduleDate([string]$dev) { Import-DemoSchedule; return $script:DemoSchedule[$dev] } function Read-DemoTeamState([string]$path) { $state = @{ Day = ''; NextAction = (Get-Date).ToString("o"); Serial = 0; Fixed = [System.Collections.Generic.HashSet[string]]::new(); Moves = @{} } if (Test-Path $path) { try { $j = [System.IO.File]::ReadAllText($path) | ConvertFrom-Json $state.Day = "$($j.Day)"; $state.NextAction = "$($j.NextAction)"; $state.Serial = [int]$j.Serial foreach ($f in @($j.Fixed)) { if ($f) { $null = $state.Fixed.Add($f) } } if ($j.Moves) { foreach ($m in $j.Moves.PSObject.Properties) { $state.Moves[$m.Name] = [int]$m.Value } } } catch {} } return $state } function Save-DemoTeamState([string]$path, $state, $roster) { # Forget fixes and move counts for devices that are no longer tracked $live = [System.Collections.Generic.HashSet[string]]::new() foreach ($t in $roster.Values) { foreach ($d in $t.devices) { $null = $live.Add($d) } } $moves = [ordered]@{} foreach ($k in $state.Moves.Keys) { if ($live.Contains($k)) { $moves[$k] = $state.Moves[$k] } } $out = [ordered]@{ Day = $state.Day; NextAction = $state.NextAction; Serial = $state.Serial Fixed = @($state.Fixed | Where-Object { $live.Contains($_) }) Moves = $moves } [System.IO.File]::WriteAllText($path, ($out | ConvertTo-Json -Depth 4), [System.Text.Encoding]::UTF8) } # Called by the worker at the start of every demo cycle function Invoke-DemoTeam([string]$DataDir) { $trackedPath = Join-Path $DataDir "Tracked-Devices.json" $statePath = Join-Path $DataDir "Demo-Team.json" if (-not (Test-Path $trackedPath)) { return } $state = Read-DemoTeamState $statePath try { $roster = ConvertTo-DemoRoster ([System.IO.File]::ReadAllText($trackedPath) | ConvertFrom-Json) } catch { return } Set-DemoSchedule $roster $state.Fixed -Pin $log = [System.Collections.Generic.List[string]]::new() $now = Get-Date $todayKey = $now.ToString("yyyy-MM-dd") $dirty = -not (Test-Path $statePath) if ($state.Day -ne $todayKey) { Invoke-DemoRollover $roster $state $log $state.Day = $todayKey $dirty = $true } $ws = [timespan]$script:DemoTeamCfg.WorkStart; $we = [timespan]$script:DemoTeamCfg.WorkEnd $working = (Test-DemoWorkday $now) -and $now.TimeOfDay -ge $ws -and $now.TimeOfDay -lt $we $next = [datetime]::MinValue if (-not [datetime]::TryParse($state.NextAction, [ref]$next)) { $next = $now } if ($working -and $now -ge $next) { Invoke-DemoTeamAction $roster $state $log $range = @($script:DemoTeamCfg.ActionEveryMinutes) $state.NextAction = $now.AddMinutes((Get-Random -Minimum ([int]$range[0]) -Maximum ([int]$range[1] + 1))).ToString("o") $dirty = $true } $script:DemoSchedulePinned = $false $script:DemoScheduleChecked = [datetime]::MinValue # re-read the roster on the next lookup if (-not $dirty -and $log.Count -eq 0) { return } if ($log.Count) { Save-DemoRoster $trackedPath $roster } Save-DemoTeamState $statePath $state $roster foreach ($line in $log) { Write-Host $line -ForegroundColor Magenta } }