CMM: wire per-bay settings restore into the imaging flow

Restore-CMM.ps1 (new) restores a CMM's PC-DMIS + goCMM settings at imaging from
its staged backup. Self-gating: reads C:\Enrollment\cmm\{cmmid,version,doda,
partgroup}.txt, skips DODA bays and bays with no staged backup, and restores
ONLY the config-version PC-DMIS zip via the existing Install-*Settings scripts.
Same-bay restore (cmmid match) so the backed-up controller CommPort is this
bay's own value - no cross-bay clobber.

Version selection matches the VERSION FIELD of the zip name, anchored on the
trailing timestamp, so version=2026 does not false-match a 2019/2016 zip whose
backup timestamp (20260612...) merely contains "2026".

09-Setup-CMM.ps1: new Step 2.8 calls Restore-CMM after app install + first-run
init (so a restored config is not clobbered by PC-DMIS defaults) and before the
C:\CMM-Install cleanup (the backup set lives under <stagingRoot>\backups\<cmmid>).
Best-effort: Restore-CMM always exits 0, imaging never fails on a restore.

startnet.cmd: stage ONLY the picked bay's backup into C:\CMM-Install\backups\
%CMMID% (the bulk robocopy now /XD-excludes the backups tree, which holds every
bay's backup - some 240 MB each - to avoid copying GBs to every imaged CMM).
Also bump the PPKG to v4.16 (the live boot.wim was already v4.16; the repo had
drifted to v4.14).

sync-cmm-backups.sh: source the backups from pxe-images/cmm/backups (where
Backup-CMM writes via the pulled-down copies), not the old cmm-bk path.

Smoke tested on the win11 VM against CMM3's real backup: version=2019 restored
the 2019 R2 zip (not 2016.0), imported HKLM+HKCU reg, converted the part-group
S:\ path to the tsgwp00525 UNC, created C:\geaofi, exit 0; version=2026 correctly
found no matching zip (anchor works).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-06-12 15:34:10 -04:00
parent 59deaea714
commit c2538a05c5
4 changed files with 145 additions and 5 deletions

View File

@@ -321,6 +321,28 @@ if (Test-Path $goCmmKey) {
}
}
# ============================================================================
# Step 2.8: Restore this bay's PC-DMIS + goCMM settings from its backup
# ============================================================================
# Runs AFTER app install + first-run-init (so a restored config is not clobbered
# by PC-DMIS's first-launch defaults) and BEFORE the Step 3 cleanup (the backup
# set lives under $stagingRoot\backups\<cmmid>, deleted by cleanup). Restore-CMM
# self-gates: it skips DODA bays and bays with no staged backup, and restores
# ONLY the config-version PC-DMIS zip. Same-bay restore -> no CommPort clobber.
# Best-effort: Restore-CMM always exits 0, so imaging never fails on a restore.
$restoreScript = Join-Path $PSScriptRoot 'scripts\Restore-CMM.ps1'
if (Test-Path -LiteralPath $restoreScript) {
Write-CMMLog "Running per-bay settings restore (Restore-CMM.ps1)"
try {
& $restoreScript -BackupRoot (Join-Path $stagingRoot 'backups') *>&1 | ForEach-Object { Write-CMMLog " $_" }
Write-CMMLog "Restore-CMM returned $LASTEXITCODE"
} catch {
Write-CMMLog "Restore-CMM threw (non-fatal): $_" 'WARN'
}
} else {
Write-CMMLog "Restore-CMM.ps1 not found at $restoreScript - skipping settings restore" 'WARN'
}
# ============================================================================
# Step 3: Conditional cleanup of the bootstrap staging dir
# ============================================================================

View File

@@ -0,0 +1,107 @@
<#
Restore-CMM.ps1
Imaging-time per-bay settings restore. Called by 09-Setup-CMM after app install,
before the C:\CMM-Install cleanup (it reads the staged backup from there).
Reads the resolved bay identity from C:\Enrollment\cmm\ (written by
resolve-cmm-bay-config.ps1 at the WinPE picker):
cmmid.txt - which staged backup set to use (e.g. CMM3)
version.txt - which PC-DMIS version to restore. A bay's backup holds every
version it ever had (2016.0 + 2019 R2); we restore ONLY the
one the bay-config pins, matched by substring (2019 -> the
'2019 R2' zip, 2016 -> the '2016.0' zip).
doda.txt - 'no' to proceed; anything else SKIPS restore (DODA bays are
handled separately and must not get a settings overlay).
partgroup.txt - optional goCMM Selected Part Group (friendly S:\ form), used
as the authoritative per-bay override even if the backup is stale.
Finds the backup set at <BackupRoot>\<cmmid>\ (default C:\CMM-Install\backups,
robocopied there from the PXE share by startnet) and restores via the sibling
Install-PCDMISSettings.ps1 + Install-goCMMSettings.ps1.
SAME-BAY restore by design (cmmid match), so the backed-up controller CommPort
is this bay's own value - no cross-bay CommPort clobber. (The clobber problem is
only when one bay's settings are pushed onto a different bay.)
Best-effort: always exits 0. Imaging must never fail because a restore step
could not find a backup or hit a non-fatal error - those are logged.
#>
param(
[string]$EnrollDir = 'C:\Enrollment\cmm',
[string]$BackupRoot = 'C:\CMM-Install\backups',
[string]$TargetUser = 'ShopFloor',
[string]$PartGroupShareRoot = '\\tsgwp00525.wjs.geaerospace.net\SHARED',
[string]$OutDir = 'C:\Logs\CMM'
)
$ErrorActionPreference = 'Continue'
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
New-Item -ItemType Directory -Path $OutDir -Force -EA SilentlyContinue | Out-Null
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
$log = Join-Path $OutDir "restore-cmm-$ts.log"
function Log($m){ $line = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] $m"; Write-Host $line; $line | Out-File -FilePath $log -Append -Encoding ascii }
function ReadTxt($n){ $p = Join-Path $EnrollDir $n; if (Test-Path -LiteralPath $p) { (Get-Content -LiteralPath $p -First 1 -EA 0).Trim() } else { '' } }
Log "==== CMM settings restore on $env:COMPUTERNAME ===="
$cmmid = ReadTxt 'cmmid.txt'
$ver = ReadTxt 'version.txt'
$doda = (ReadTxt 'doda.txt').ToLower()
$pgRaw = ReadTxt 'partgroup.txt'
if (-not $cmmid) { Log "no cmmid.txt (manual CMM id, or not a bay-config bay) - nothing to restore. Skipping."; exit 0 }
if ($doda -eq 'yes') { Log "DODA bay ($cmmid) - skipping settings restore by policy."; exit 0 }
$bdir = Join-Path $BackupRoot $cmmid
if (-not (Test-Path -LiteralPath $bdir)) { Log "no staged backup at $bdir - skipping (stage it with sync-cmm-backups.sh)."; exit 0 }
Log "bay=$cmmid version=$ver doda=$doda backupDir=$bdir"
# --- PC-DMIS: restore ONLY the config-version zip ---
# A bay's folder holds every version it ever had (e.g. 2016.0 + 2019 R2). Match
# the VERSION FIELD of the name, not a loose substring: filenames are
# pcdmis_backup_<PC>_<VER>_<YYYYMMDD-HHMMSS>.zip
# A loose "*2026*" would wrongly match a 2019 zip whose TIMESTAMP starts 2026
# (e.g. 20260612). Anchor on the trailing timestamp so only the real version
# field (2019 -> '2019 R2', 2016 -> '2016.0', 2026 -> '2026.1') matches.
$pcZip = $null
if ($ver) {
$verRx = '^pcdmis_backup_.+_' + [regex]::Escape($ver) + '[^_]*_\d{8}-\d{6}\.zip$'
$pcZip = Get-ChildItem -LiteralPath $bdir -Filter 'pcdmis_backup_*.zip' -File -EA 0 |
Where-Object { $_.Name -match $verRx } | Sort-Object LastWriteTime | Select-Object -Last 1
}
$pcScript = Join-Path $here 'Install-PCDMISSettings.ps1'
if (-not $pcZip) {
Log "WARN: no PC-DMIS backup zip matching version '$ver' in $bdir - PC-DMIS settings NOT restored."
} elseif (-not (Test-Path -LiteralPath $pcScript)) {
Log "WARN: Install-PCDMISSettings.ps1 not found at $pcScript - PC-DMIS settings NOT restored."
} else {
Log "restoring PC-DMIS $ver from $($pcZip.Name) (TargetUser=$TargetUser)"
try { & $pcScript -BackupPath $pcZip.FullName -TargetUser $TargetUser *>&1 | ForEach-Object { Log " $_" } }
catch { Log " ERROR in Install-PCDMISSettings: $($_.Exception.Message)" }
}
# --- goCMM: restore the bay's settings; force the authoritative part group ---
$goZip = Get-ChildItem -LiteralPath $bdir -Filter 'gocmm_backup_*.zip' -File -EA 0 | Sort-Object LastWriteTime | Select-Object -Last 1
$goScript = Join-Path $here 'Install-goCMMSettings.ps1'
if (-not $goZip) {
Log "WARN: no goCMM backup zip in $bdir - goCMM settings NOT restored."
} elseif (-not (Test-Path -LiteralPath $goScript)) {
Log "WARN: Install-goCMMSettings.ps1 not found at $goScript - goCMM settings NOT restored."
} else {
# partgroup.txt is the friendly S:\... form; convert the S: prefix to the UNC
# share root the same way 09-Setup-CMM does, and pass it so the restored
# Selected Part Group matches the CURRENT bay-config even if the backup is stale.
$goArgs = @{ BackupPath = $goZip.FullName }
if ($pgRaw) {
$pg = $pgRaw -replace '(?i)^S:\\', "$PartGroupShareRoot\"
$goArgs.SelectedPartGroup = $pg
Log "restoring goCMM from $($goZip.Name) (SelectedPartGroup=$pg)"
} else {
Log "restoring goCMM from $($goZip.Name) (no partgroup override - using backup's own value)"
}
try { & $goScript @goArgs *>&1 | ForEach-Object { Log " $_" } }
catch { Log " ERROR in Install-goCMMSettings: $($_.Exception.Message)" }
}
Log "==== restore complete for $cmmid ===="
exit 0

View File

@@ -181,8 +181,8 @@ REM --- PPKG configuration (constructed at menu time, see docs) ---
REM Vendor ships one source PPKG; we construct the BPRT-tagged filename
REM by filling in Office, Region, Expiry, Version on the target copy.
REM Update SOURCE_PPKG + PPKG_VER when a new PPKG is released.
set SOURCE_PPKG=GCCH_Prod_SFLD_v4.14.ppkg
set PPKG_VER=v4.14
set SOURCE_PPKG=GCCH_Prod_SFLD_v4.16.ppkg
set PPKG_VER=v4.16
set PPKG_EXP=20260831
set REGION=US
@@ -524,13 +524,24 @@ REM unified GE-Enforce dispatcher takes over from the share for ongoing updates.
if /i not "%PCTYPE%"=="gea-shopfloor-cmm" goto skip_cmm_stage
if exist "Y:\installers-post\cmm\cmm-manifest.json" (
mkdir W:\CMM-Install 2>NUL
robocopy "Y:\installers-post\cmm" "W:\CMM-Install" /E /MT:16 /R:1 /W:1 /NFL /NDL /LOG+:"%STAGELOG%"
REM /XD the backups tree: it holds EVERY bay's settings backup (some 240 MB
REM each). Copying all of it to every imaged CMM would waste GBs. The bulk
REM copy excludes it; the selected bay's backup is staged separately below.
robocopy "Y:\installers-post\cmm" "W:\CMM-Install" /E /XD "Y:\installers-post\cmm\backups" /MT:16 /R:1 /W:1 /NFL /NDL /LOG+:"%STAGELOG%"
if errorlevel 8 echo WARNING: cmm robocopy exit %ERRORLEVEL%
echo Staged CMM bootstrap to W:\CMM-Install.
echo [%TIME%] Staged CMM bootstrap >> "%STAGELOG%"
) else (
echo WARNING: Y:\cmm-installers not found - CMM PC cannot install Hexagon apps at imaging time.
)
REM Stage ONLY the selected bay's settings backup. 09-Setup-CMM's Restore-CMM
REM reads W:\CMM-Install\backups\%CMMID%\ to restore PC-DMIS + goCMM settings.
if not defined CMMID goto skip_cmm_stage
if exist "Y:\installers-post\cmm\backups\%CMMID%" (
robocopy "Y:\installers-post\cmm\backups\%CMMID%" "W:\CMM-Install\backups\%CMMID%" /E /MT:16 /R:1 /W:1 /NFL /NDL /LOG+:"%STAGELOG%"
echo Staged %CMMID% settings backup for restore.
echo [%TIME%] Staged %CMMID% backup >> "%STAGELOG%"
)
:skip_cmm_stage
REM --- Stage Keyence per-model bootstrap bundle (Keyence PCs only) ---

View File

@@ -5,7 +5,7 @@
#
# Source layout - one folder per CMM, named by the cmm_id in cmm-bay-config.csv,
# produced by Backup-CMM.ps1 and mirrored here from the live bays:
# /home/camp/pxe-images/cmm-bk/<cmm_id>/
# /home/camp/pxe-images/cmm/backups/<cmm_id>/
# gocmm_backup_<PC>_<ts>.zip
# pcdmis_backup_<PC>_<ver>_<ts>.zip (one per installed PC-DMIS version)
# cmm-backup-index.json
@@ -23,7 +23,7 @@ set -euo pipefail
PXE_HOST="${PXE_HOST:-172.16.9.1}"
PXE_USER="${PXE_USER:-pxe}"
PXE_PASS="${PXE_PASS:-pxe}"
BACKUP_SRC_DIR="${CMM_BACKUP_DIR:-/home/camp/pxe-images/cmm-bk}"
BACKUP_SRC_DIR="${CMM_BACKUP_DIR:-/home/camp/pxe-images/cmm/backups}"
REMOTE_BASE="/srv/samba/enrollment/installers-post/cmm/backups"
ONLY_ID="${CMM_ID:-}"