CMM/DODA: Install-DODA does the full unattended runtime+app install

The vendor GE_DODA_build5_Installer.exe is a multi-app NSIS bundler whose /S
silent mode is broken (nested sub-install -> MSI 1619); VM-confirmed it only
works interactively. So Install-DODA now drives the bundled components directly,
all silent:
  python-2.7.16 msi /qn  -> C:\Python27
  openjdk-1.8.0.232 msi /qn -> C:\Program Files\RedHat
  gs927w64.exe /S        -> Ghostscript 9.27
  vc_redist /quiet
  doda_build*.zip        -> C:\Apps\DODA  (+ writes .settings the zip lacks)
Then sets AFDA_INSTALL_DIR + Java PATH, precompiles .py->.pyc as admin, and
grants Users+Auth Users Full on C:\Apps\DODA so the locked-down operator can run
DovetailAnalysis.exe (writes .pyc/PreProcess/$TMP). Each step detects-and-skips;
idempotent. VM-validated end to end incl a non-admin operator-paths test.

Runtime binaries (python/jdk/gs/vc msis + doda zip) are staged on the share, not
committed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-06-17 19:54:15 -04:00
parent b5b644a360
commit 6e218f3daf

View File

@@ -1,59 +1,165 @@
# Install-DODA.ps1 - Extract DODA zip to C:\Apps\DODA\.
# Install-DODA.ps1 - full unattended DODA install (runtimes + app + config + ACLs).
#
# Called by Install-FromManifest as a Type=PS1 entry. The zip is staged
# alongside this script in C:\CMM-Install\ by startnet.cmd.
# The vendor GE_DODA_build5_Installer.exe is a multi-app NSIS bundler. Its /S
# silent mode is BROKEN (a nested sub-install gets a bad package path -> MSI
# error 1619), so it cannot be used for imaging. Each bundled component installs
# cleanly on its own, so we drive them directly here. (VM-validated 2026-06-17.)
#
# Components staged in C:\CMM-Install (robocopied from the SFLD/enrollment share
# by startnet.cmd), all installed SILENTLY:
# python-2.7.16.amd64.msi -> C:\Python27 (msiexec /qn)
# java-1.8.0-openjdk-*.msi -> C:\Program Files\RedHat (msiexec /qn)
# gs927w64.exe -> C:\Program Files\gs\gs9.27 (/S)
# vc_redist.x64.exe -> VC++ runtime (/quiet)
# doda_build*.zip -> C:\Apps\DODA (app files)
#
# DODA at runtime is DovetailAnalysis.exe -> JVM (OpenJDK) + C:\Python27 python +
# Ghostscript (PS->PDF). .settings (NOT in the zip - the NSIS installer wrote it)
# hardcodes those paths; we write it here.
#
# LOCKDOWN: DovetailAnalysis.exe runs as the locked-down OPERATOR (not admin) and
# writes throughout C:\Apps\DODA (.pyc, PreProcess) + $TMP. We (a) grant Users +
# Authenticated Users Full on C:\Apps\DODA and (b) PRECOMPILE the .py to .pyc as
# admin so the operator never needs to write bytecode into a protected path.
#
# Run as administrator / SYSTEM (imaging or GE-Enforce). Idempotent: each step
# detects-and-skips if already present, and ACL/.settings/env re-assert every run
# (so a post-lockdown re-run repairs a stripped ACE).
$ErrorActionPreference = 'Continue'
# The doda zip + runtime installers are staged ALONGSIDE this script (same dir).
# When the manifest runs it, that dir is C:\CMM-Install (= $PSScriptRoot).
$stagingRoot = $PSScriptRoot
if (-not $stagingRoot -or -not (Test-Path $stagingRoot)) { $stagingRoot = 'C:\CMM-Install' }
$installDir = 'C:\Apps\DODA'
$zipPattern = 'doda_build*.zip'
$stagingRoot = Split-Path $PSScriptRoot -ErrorAction SilentlyContinue
if (-not $stagingRoot) { $stagingRoot = 'C:\CMM-Install' }
$python27 = 'C:\Python27\python.exe'
$gsExe = 'C:\Program Files\gs\gs9.27\bin\gswin64c.exe'
$zip = Get-ChildItem -Path $stagingRoot -Filter $zipPattern -File -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $zip) {
Write-Host "DODA zip not found in $stagingRoot (pattern: $zipPattern)"
exit 1
$logDir = 'C:\Logs\CMM'
New-Item -ItemType Directory -Path $logDir -Force -EA SilentlyContinue | Out-Null
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
$log = Join-Path $logDir "doda-install-$ts.log"
function Log($m){ $line = "[{0}] {1}" -f (Get-Date -Format 'HH:mm:ss'), $m; Write-Host $line; Add-Content -Path $log -Value $line -EA SilentlyContinue }
function Find-One($pattern) {
Get-ChildItem -Path $stagingRoot -Filter $pattern -File -EA SilentlyContinue | Select-Object -First 1
}
if (-not (Test-Path $installDir)) {
New-Item -Path $installDir -ItemType Directory -Force | Out-Null
}
Log "==== DODA install on $env:COMPUTERNAME (staging=$stagingRoot) ===="
Write-Host "Extracting $($zip.Name) to $installDir..."
try {
Expand-Archive -LiteralPath $zip.FullName -DestinationPath $installDir -Force -ErrorAction Stop
Write-Host "DODA extracted to $installDir"
} catch {
Write-Host "ERROR: Extract failed - $_"
exit 1
}
# MergeFiles.exe (cmm-utilities toolchain) reads C:\Apps\DODA\PreProcess\ as
# its working dir. The DODA zip extracts flat without it, so create it here -
# a missing PreProcess dir is the known cause of MergeFiles.GetDoDAFolder
# throwing DirectoryNotFoundException (see cmm-utilities dotNET event.txt).
$preProcess = Join-Path $installDir 'PreProcess'
if (-not (Test-Path $preProcess)) {
New-Item -Path $preProcess -ItemType Directory -Force | Out-Null
Write-Host "Created $preProcess"
}
# DODA runs as the LOCKED-DOWN OPERATOR (not admin) and writes throughout its
# install folder at runtime - PreProcess\ (MergeFiles GetDoDAFolder) plus
# output/temp elsewhere under C:\Apps\DODA. Grant Users + Authenticated Users
# Full on the WHOLE folder (object+container inherit; /T applies it to every
# existing child incl PreProcess). SIDs, not names, to stay locale-independent.
# Applied every run so it re-asserts after any lockdown pass that strips the ACE.
foreach ($sid in '*S-1-5-32-545','*S-1-5-11') { # BUILTIN\Users, NT AUTHORITY\Authenticated Users
& icacls $installDir /grant "${sid}:(OI)(CI)F" /T /C 2>&1 | Out-Null
}
Write-Host "Granted Users + Authenticated Users Full on $installDir (recursive)"
if (Test-Path (Join-Path $installDir 'DovetailAnalysis.exe')) {
Write-Host "DovetailAnalysis.exe verified present"
exit 0
# --- 1. Python 2.7 ----------------------------------------------------------
if (Test-Path $python27) {
Log "Python 2.7 already present - skip"
} else {
Write-Host "ERROR: DovetailAnalysis.exe not found after extract"
$msi = Find-One 'python-2.7*.msi'
if ($msi) {
Log "Installing Python: $($msi.Name)"
$p = Start-Process msiexec.exe -Wait -PassThru -ArgumentList @('/i', "`"$($msi.FullName)`"", '/qn', '/norestart', 'ADDLOCAL=ALL')
Log " Python msiexec exit $($p.ExitCode)"
} else { Log "WARN: python-2.7*.msi not found in $stagingRoot" }
}
# --- 2. OpenJDK 8 -----------------------------------------------------------
$jdk = Get-ChildItem 'C:\Program Files\RedHat','C:\Program Files\Java' -Directory -EA SilentlyContinue |
Where-Object Name -match 'openjdk|jdk|jre' | Select-Object -First 1
if ($jdk) {
Log "OpenJDK already present ($($jdk.Name)) - skip"
} else {
$msi = Find-One 'java-*openjdk*.msi'
if ($msi) {
Log "Installing OpenJDK: $($msi.Name)"
$p = Start-Process msiexec.exe -Wait -PassThru -ArgumentList @('/i', "`"$($msi.FullName)`"", '/qn', '/norestart')
Log " OpenJDK msiexec exit $($p.ExitCode)"
$jdk = Get-ChildItem 'C:\Program Files\RedHat','C:\Program Files\Java' -Directory -EA SilentlyContinue |
Where-Object Name -match 'openjdk|jdk|jre' | Select-Object -First 1
} else { Log "WARN: java-*openjdk*.msi not found in $stagingRoot" }
}
# --- 3. Ghostscript 9.27 ----------------------------------------------------
if (Test-Path $gsExe) {
Log "Ghostscript 9.27 already present - skip"
} else {
$gs = Find-One 'gs927w64.exe'
if ($gs) {
Log "Installing Ghostscript: $($gs.Name)"
$p = Start-Process $gs.FullName -Wait -PassThru -ArgumentList '/S'
Log " Ghostscript exit $($p.ExitCode)"
} else { Log "WARN: gs927w64.exe not found in $stagingRoot" }
}
# --- 4. VC++ redist (best-effort) ------------------------------------------
$vc = Find-One 'vc_redist.x64.exe'
if ($vc) {
Log "Installing VC++ redist: $($vc.Name)"
$p = Start-Process $vc.FullName -Wait -PassThru -ArgumentList @('/quiet', '/norestart')
Log " VC++ exit $($p.ExitCode) (1638/3010 = already-present/ok)"
} else { Log "VC++ redist not staged - skipping (usually already present)" }
# --- 5. DODA app files ------------------------------------------------------
$zip = Find-One 'doda_build*.zip'
if (-not $zip) {
Log "ERROR: doda_build*.zip not found in $stagingRoot - cannot place DODA app"
} else {
New-Item -ItemType Directory -Path $installDir -Force -EA SilentlyContinue | Out-Null
Log "Extracting $($zip.Name) -> $installDir"
try { Expand-Archive -LiteralPath $zip.FullName -DestinationPath $installDir -Force -EA Stop }
catch { Log "ERROR: extract failed - $_" }
}
# --- 6. .settings (the NSIS installer wrote this; the zip does NOT contain it) ---
$settings = Join-Path $installDir '.settings'
$settingsBody = @'
WORKING_DIR=$TMP
PYTHONEXE=C:\Python27\python.exe
OPTIONS_FILE="$WORKING_DIR/dvt_run_options.py"
RPT_GUI=$PYTHONEXE $AFDA_INSTALL_DIR/dvtRptGui.py -file=$OPTIONS_FILE
OUTPUT_PROCESSOR=$PYTHONEXE $AFDA_INSTALL_DIR/processOutput.py $OPTIONS_FILE
PS2PDF="C:\Program Files\gs\gs9.27\bin\gswin64c.exe" -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$$out $$in
'@
if (Test-Path $installDir) {
Set-Content -Path $settings -Value $settingsBody -Encoding ascii -Force
Log "Wrote $settings"
}
# --- 7. PreProcess dir (MergeFiles GetDoDAFolder reads it) ------------------
$preProcess = Join-Path $installDir 'PreProcess'
if (-not (Test-Path $preProcess)) { New-Item -ItemType Directory -Path $preProcess -Force | Out-Null; Log "Created $preProcess" }
# --- 8. Env: AFDA_INSTALL_DIR + Java on PATH (machine scope) ----------------
[Environment]::SetEnvironmentVariable('AFDA_INSTALL_DIR', $installDir, 'Machine')
Log "Set AFDA_INSTALL_DIR=$installDir (Machine)"
if ($jdk) {
$machPath = [Environment]::GetEnvironmentVariable('PATH','Machine')
foreach ($add in @((Join-Path $jdk.FullName 'bin'), (Join-Path $jdk.FullName 'jre\bin'))) {
if (Test-Path $add) {
$parts = $machPath -split ';'
if ($parts -notcontains $add) { $machPath = "$machPath;$add"; Log "PATH += $add" }
}
}
[Environment]::SetEnvironmentVariable('PATH', $machPath, 'Machine')
}
# --- 9. Precompile .py -> .pyc as admin (so the locked-down operator never
# needs write access to compile bytecode at runtime) ------------------
if ((Test-Path $python27) -and (Test-Path $installDir)) {
Log "Precompiling DODA .py with $python27"
$p = Start-Process $python27 -Wait -PassThru -ArgumentList @('-m','compileall', "`"$installDir`"") -WindowStyle Hidden
Log " compileall exit $($p.ExitCode)"
}
# --- 10. ACLs: Users + Authenticated Users Full on C:\Apps\DODA so the
# locked-down operator can run DODA (writes .pyc/PreProcess/output) ---
if (Test-Path $installDir) {
foreach ($sid in '*S-1-5-32-545','*S-1-5-11') { # BUILTIN\Users, NT AUTHORITY\Authenticated Users
& icacls $installDir /grant "${sid}:(OI)(CI)F" /T /C 2>&1 | Out-Null
}
Log "Granted Users + Authenticated Users Full on $installDir (recursive)"
}
# --- Verify -----------------------------------------------------------------
$ok = (Test-Path (Join-Path $installDir 'DovetailAnalysis.exe')) -and (Test-Path $python27) -and (Test-Path $gsExe) -and $jdk
if ($ok) { Log "==== DODA install OK (DovetailAnalysis + Python27 + Ghostscript + OpenJDK present) ===="; exit 0 }
else {
Log "==== DODA install INCOMPLETE - DovetailAnalysis=$([bool](Test-Path (Join-Path $installDir 'DovetailAnalysis.exe'))) Python=$([bool](Test-Path $python27)) GS=$([bool](Test-Path $gsExe)) JDK=$([bool]$jdk) ===="
exit 1
}