diff --git a/plugins/printers/client/Install-ShopdbPrinterDrivers.ps1 b/plugins/printers/client/Install-ShopdbPrinterDrivers.ps1 index 7984c5c..20a495a 100644 --- a/plugins/printers/client/Install-ShopdbPrinterDrivers.ps1 +++ b/plugins/printers/client/Install-ShopdbPrinterDrivers.ps1 @@ -22,6 +22,28 @@ # SHARE PATHS: on a GE-Enforce site the packages usually live on the SFLD share, # which is mounted ONLY during the enforcement cycle. Run this as a manifest # entry inside that cycle, not as its own scheduled task. +# +# WAVES, because a driver package is not a manifest check. GE-Enforce gives each +# PC a start offset of SHA256(hostname) % 5 MINUTES and then repeats every 5 +# minutes, which was sized for reading a few KB of JSON. A driver set is 100 MB +# for the two universals and 226 MB for a full site, so the day an entry lands +# the whole fleet pulls it inside one 5-minute window: roughly 30 GB across 300 +# bays, on the same share every bay needs for everything else. The failure is not +# slow drivers, it is a floor that stops converging. +# +# -WaveStart with -Waves spreads that out. Each PC derives its own wave from its +# hostname and does nothing until its turn, so there is no central coordination, +# no per-bay configuration and nothing to reconcile afterwards. The hash is the +# same idiom Register-GEEnforce.ps1 uses for its offset, SHA-256 rather than MD5 +# because FIPS-enforced bays disable MD5 outright. +# +# A bay switched off during its wave installs on its next cycle instead. Late is +# a non-event here; the wave sets the earliest moment, not a deadline. +# +# Unparseable wave arguments FAIL CLOSED - nothing installs, and it says so every +# cycle. Failing open would restore precisely the stampede this exists to avoid, +# and 30 GB cannot be un-sent, whereas a typo that installs nothing is loud in +# the log and fixed in a minute. param( # Defaults to drivers.json beside this script. @@ -32,8 +54,26 @@ param( [string[]]$Only = @(), # Report what is missing and change nothing. This is what a DSC TestScript - # calls: exit 0 means compliant. - [switch]$TestOnly + # calls: exit 0 means compliant. A bay whose wave has not arrived is + # compliant BY DESIGN - not installed is its desired state today. + [switch]$TestOnly, + + # Date the rollout opens, e.g. '2026-08-25' or '2026-08-25 22:00'. Empty + # means no gating at all, which is right for imaging time: a bay being built + # is one bay, and it should come off the line complete. + [string]$WaveStart = '', + + # How many waves to spread the fleet across. 0 or 1 means everyone at once. + # 300 bays over 10 daily waves is ~30 bays and ~3 GB a day. + [int]$Waves = 0, + + # Wave spacing. Hours finishes a 10-wave rollout inside a day; Days is the + # cautious setting when nobody is watching the share. + [ValidateSet('Days', 'Hours')] + [string]$WaveUnit = 'Days', + + # Pilot bays skip the gate. Use this to prove one bay before the fleet. + [switch]$IgnoreWave ) $ErrorActionPreference = 'Continue' @@ -51,6 +91,42 @@ function Log([string]$msg) { "$ts [set] $msg" | Tee-Object -FilePath $logFile -Append | Out-Null } +# The gate runs BEFORE the manifest is read, because the manifest lives on the +# share too. A bay that is not due this wave must not touch the share at all - +# reading it is cheap, but 300 bays deciding to read it in the same 5 minutes is +# how this whole problem starts. +if ($WaveStart -and -not $IgnoreWave) { + $start = [datetime]::MinValue + if (-not [datetime]::TryParse($WaveStart, [ref]$start)) { + Log "ERROR -WaveStart '$WaveStart' is not a date. Refusing to install." + Log ' (failing closed on purpose: guessing here would release the whole fleet at once)' + exit 1 + } + + $waveCount = $Waves + if ($waveCount -lt 1) { $waveCount = 1 } + + # Same hostname hash as the enforcer's own start offset, so a bay's wave is + # stable for the life of its name: it cannot drift between cycles, and a + # rerun never moves a PC into a different wave. + $hostHash = [System.BitConverter]::ToUInt32( + [System.Security.Cryptography.SHA256]::Create().ComputeHash( + [System.Text.Encoding]::UTF8.GetBytes([System.Environment]::MachineName)), 0) + $wave = [int]($hostHash % [uint32]$waveCount) + + if ($WaveUnit -eq 'Hours') { $due = $start.AddHours($wave) } + else { $due = $start.AddDays($wave) } + + if ((Get-Date) -lt $due) { + Log ("wave {0} of {1} for {2}; not due until {3}. Nothing to do." -f ` + $wave, $waveCount, [System.Environment]::MachineName, + $due.ToString('yyyy-MM-dd HH:mm')) + exit 0 + } + Log ("wave {0} of {1} opened {2}; proceeding" -f ` + $wave, $waveCount, $due.ToString('yyyy-MM-dd HH:mm')) +} + if (-not (Test-Path $Manifest)) { Log "ERROR manifest not found: $Manifest" exit 1