Generic build for other GE sites: no WJ data, one shopfloor type, Windows USB builder
Everything West Jefferson specific moved out to a separate repo (pxe-site-wj): NTLARS/UDC/heat-treat backups keyed by WJ machine numbers, CMM and Wax/Trace bay lists, WJ OpenText profiles, site-config.json, the WJ preinstall list, and the WJ shopfloor PC types (Collections, NoCollections, Common, Keyence, CMM, Genspect, HeatTreat, Wax/Trace, Display, PartMarker) with GE-Enforce, machine numbers and the desktop layout. The part-marker bench tools moved to their own repo. Dead scripts removed: build-proxmox-iso, deploy-bios, sync_hardware_models, mirror-from-gold, pull-bios, startnet-template.cmd. Shopfloor is one generic type, gea-shopfloor-standard, with today's timing: - PPKG at first logon (unattend Order 10, unchanged). - Install-AppList.ps1 runs C:\PreInstall\preinstall.json during setup. - A new SYSTEM task, GE Shopfloor Post-PPKG, runs postppkg.json once after the post-PPKG reboot. It also clears AutoLogonCount after the ShopFloor handover (the Users-level monitor can be denied that HKLM write) and removes the Intune Sync task when done. - Both lists ship empty. A site adds entries and installers. - WinPE choice 3 goes straight to the PPKG menu. Staging, fetch and heal carry only the generic payload. The webapp's shopfloor sub-menu editor is gone, and the imaging tiles name the new stages. - The playbook deploys the empty lists only if missing. It now deploys the PowerShell 7 MSI the shopfloor unattend installs, which it never did before. Blancco site settings move into the webapp (Updates > Blancco releases): business location, licensing Wi-Fi SSID and password. They are rendered into the served preferences.xml and applied to every staged release, and are seeded from site-vars (site_name, blancco_wifi_ssid, blancco_wifi_password) only while none exist. The repo XML carries no site values; the plaintext Wi-Fi password is gone from it. New site_name and site_timezone settings: report and evidence-pack site name, server clock and webapp time zone. No more West Jefferson defaults in webapp/config.py or the dashboard title. Sync-BPRT.ps1 gets the server address filled in at download instead of WJ's GE address. USB on Windows: - scripts/Build-PxeUsb.ps1 runs the same checks as build-usb.sh. It writes one FAT32 CIDATA partition (ISO files plus patched grub.cfg plus payload) with diskpart and reads every file back by SHA-256. - Tested with Windows PowerShell 5.1 in the win11 VM, and the layout installs and first-boots in the pxetest VM. - scripts/stage-usb-share.sh stages the build folder on a share. - site-content/ is optional for both builders. KB docs/kb/How_To_Deploy_PXE_Server_New_Site.html: new-site deployment, Windows only, with a site-vars.yml guide. Checks: webapp tests pass (5 suites); the unattend lint and playbook syntax checks pass; every .ps1 parses; there are no WJ hosts, IPs, names or PC types in playbook/, webapp/, scripts/ or autoinstall/.
This commit is contained in:
@@ -1,144 +0,0 @@
|
||||
# Get-PCProfile.ps1 - Shared helper for resolving the active PC profile
|
||||
# from site-config.json. Dot-source this from any script that needs
|
||||
# profile-aware configuration:
|
||||
#
|
||||
# . "$PSScriptRoot\lib\Get-PCProfile.ps1" (from Shopfloor\ scripts)
|
||||
# . "$PSScriptRoot\Get-PCProfile.ps1" (from lib\ scripts)
|
||||
#
|
||||
# After dot-sourcing, these variables are available:
|
||||
# $siteConfig - full parsed site-config.json (or $null)
|
||||
# $pcType - from pc-type.txt (e.g. "Standard", "CMM", "Display")
|
||||
# $pcSubtype - from pc-subtype.txt (e.g. "Timeclock", "Machine", "Lobby")
|
||||
# $profileKey - lookup key (e.g. "Standard-Machine", "CMM", "Display-Lobby")
|
||||
# $pcProfile - the matched profile object from pcProfiles, or $null
|
||||
#
|
||||
# Resolution order for any config key (e.g. startupItems):
|
||||
# 1. $pcProfile.startupItems (profile-specific override)
|
||||
# 2. $siteConfig.startupItems (site-wide default)
|
||||
# 3. (hardcoded fallback in the calling script)
|
||||
#
|
||||
# Usage pattern in consuming scripts:
|
||||
# $items = Get-ProfileValue 'startupItems'
|
||||
# if ($null -ne $items) { ... } else { <hardcoded fallback> }
|
||||
#
|
||||
# Get-ProfileValue distinguishes "not set" ($null) from "explicitly empty"
|
||||
# (@()) so a profile can opt out of a site-wide default by setting the
|
||||
# key to []. Do NOT use truthiness checks like `if ($pcProfile.startupItems)`
|
||||
# because an empty array is falsy and would fall through to site defaults.
|
||||
|
||||
function Get-ProfileValue {
|
||||
param([Parameter(Mandatory)][string]$Key)
|
||||
if ($pcProfile -and ($pcProfile.PSObject.Properties.Name -contains $Key)) {
|
||||
return ,@($pcProfile.$Key)
|
||||
}
|
||||
if ($siteConfig -and ($siteConfig.PSObject.Properties.Name -contains $Key)) {
|
||||
return ,@($siteConfig.$Key)
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-SiteConfig {
|
||||
$configPath = 'C:\Enrollment\site-config.json'
|
||||
if (-not (Test-Path -LiteralPath $configPath)) {
|
||||
Write-Host "site-config.json not found - using defaults" -ForegroundColor DarkGray
|
||||
return $null
|
||||
}
|
||||
try {
|
||||
return (Get-Content -LiteralPath $configPath -Raw -ErrorAction Stop | ConvertFrom-Json)
|
||||
} catch {
|
||||
Write-Warning "Failed to parse site-config.json: $_"
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
$siteConfig = Get-SiteConfig
|
||||
|
||||
# Read PC type + sub-type from the files startnet.cmd wrote during imaging
|
||||
$pcType = ''
|
||||
$pcSubtype = ''
|
||||
$typeFile = 'C:\Enrollment\pc-type.txt'
|
||||
$subtypeFile = 'C:\Enrollment\pc-subtype.txt'
|
||||
|
||||
if (Test-Path -LiteralPath $typeFile) {
|
||||
$pcType = (Get-Content -LiteralPath $typeFile -First 1 -ErrorAction SilentlyContinue).Trim()
|
||||
}
|
||||
if (Test-Path -LiteralPath $subtypeFile) {
|
||||
$pcSubtype = (Get-Content -LiteralPath $subtypeFile -First 1 -ErrorAction SilentlyContinue).Trim()
|
||||
}
|
||||
|
||||
# Display sub-type fallback: if pc-subtype.txt is absent (post-rename-reorg
|
||||
# default) but display-type.txt exists, use it as the subtype. Lets the
|
||||
# Display-Lobby / Display-Dashboard / gea-shopfloor-display-{lobby,dashboard}
|
||||
# profile keys resolve correctly for Display PCs.
|
||||
$displayTypeFile = 'C:\Enrollment\display-type.txt'
|
||||
if (-not $pcSubtype -and ($pcType -ieq 'gea-shopfloor-display' -or $pcType -ieq 'Display') -and (Test-Path -LiteralPath $displayTypeFile)) {
|
||||
$pcSubtype = (Get-Content -LiteralPath $displayTypeFile -First 1 -ErrorAction SilentlyContinue).Trim()
|
||||
}
|
||||
|
||||
# Build the profile key: "Standard-Machine", "CMM", "Display-Lobby", etc.
|
||||
$profileKey = if ($pcSubtype) { "$pcType-$pcSubtype" } else { $pcType }
|
||||
|
||||
# 2026-05-03 rename: when the legacy key (e.g. "Standard-Machine") is not
|
||||
# present in pcProfiles, fall through to the new gea-shopfloor-* equivalent
|
||||
# (e.g. "gea-shopfloor-collections"). Vice versa: a fleet PC that already
|
||||
# writes "gea-shopfloor-collections" finds its profile under either key.
|
||||
$pcProfileAliasGroups = @(
|
||||
@('Standard-Machine', 'gea-shopfloor-collections', 'gea-shopfloor-nocollections'),
|
||||
@('Standard-Timeclock', 'gea-shopfloor-common'),
|
||||
@('Lab', 'gea-shopfloor-common'),
|
||||
@('CMM', 'gea-shopfloor-cmm'),
|
||||
@('Keyence', 'gea-shopfloor-keyence'),
|
||||
@('WaxAndTrace', 'gea-shopfloor-waxtrace'),
|
||||
@('Genspect', 'gea-shopfloor-genspect'),
|
||||
@('Display', 'gea-shopfloor-display'),
|
||||
@('Display-Lobby', 'gea-shopfloor-display-Lobby', 'gea-shopfloor-display-lobby'),
|
||||
@('Display-Dashboard', 'gea-shopfloor-display-Dashboard', 'gea-shopfloor-display-dashboard'),
|
||||
@('Heattreat', 'gea-shopfloor-heattreat')
|
||||
)
|
||||
|
||||
# Look up the profile in pcProfiles. Fall back to $null (callers use
|
||||
# site-wide defaults or hardcoded values).
|
||||
$pcProfile = $null
|
||||
if ($siteConfig -and $siteConfig.pcProfiles -and $profileKey) {
|
||||
$pcProfile = $siteConfig.pcProfiles.$profileKey
|
||||
if (-not $pcProfile) {
|
||||
foreach ($g in $pcProfileAliasGroups) {
|
||||
if ($g -icontains $profileKey) {
|
||||
foreach ($alias in $g) {
|
||||
if ($alias -ieq $profileKey) { continue }
|
||||
$candidate = $siteConfig.pcProfiles.$alias
|
||||
if ($candidate) { $pcProfile = $candidate; break }
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
# Subtype-strip fallback: a compound key like "gea-shopfloor-cmm-doda" (DODA
|
||||
# CMM bays set pc-subtype.txt=doda) has no profile of its own. Fall back to
|
||||
# the BARE pc-type ("gea-shopfloor-cmm" -> "CMM"). Without this the profile
|
||||
# resolves to $null and callers silently use hardcoded defaults (wrong
|
||||
# desktop icons + unwanted startup items).
|
||||
if (-not $pcProfile -and $pcSubtype) {
|
||||
$bare = $pcType
|
||||
$pcProfile = $siteConfig.pcProfiles.$bare
|
||||
if (-not $pcProfile) {
|
||||
foreach ($g in $pcProfileAliasGroups) {
|
||||
if ($g -icontains $bare) {
|
||||
foreach ($alias in $g) {
|
||||
if ($alias -ieq $bare) { continue }
|
||||
$candidate = $siteConfig.pcProfiles.$alias
|
||||
if ($candidate) { $pcProfile = $candidate; break }
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($pcProfile) { $profileKey = $bare }
|
||||
}
|
||||
}
|
||||
|
||||
if ($pcProfile) {
|
||||
Write-Host "PC profile: $profileKey" -ForegroundColor Cyan
|
||||
} elseif ($profileKey) {
|
||||
Write-Host "PC profile '$profileKey' not found in site-config.json - using site defaults" -ForegroundColor DarkGray
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
# Invoke-FilteredReportIP.ps1
|
||||
# Reports this bay's GE corp IP to the GE Tines webhook with a tight
|
||||
# subnet filter so PXE LAN (10.9.100.x) or any other 10.x stray address
|
||||
# never leaks. Mirrors the payload shape of GE's Intune-deployed
|
||||
# ReportIPAddresses_v2.ps1 so the receiving Tines workflow accepts it.
|
||||
#
|
||||
# WHY:
|
||||
# GE's signed Report IP script filters Get-NetIPAddress with
|
||||
# $_.StartsWith("10.") - too broad for WJ where PXE LAN is also 10.x.
|
||||
# Calling our own POST with a narrow filter guarantees the webhook
|
||||
# only ever sees the bay's AESFMA corp 10.x address.
|
||||
#
|
||||
# ALLOWED RANGES (WJ-specific - update if site moves to a new VLAN):
|
||||
# 10.134.48.0/23 (10.134.48.0 - 10.134.49.255)
|
||||
# 10.48.249.0/26 (10.48.249.0 - 10.48.249.63)
|
||||
|
||||
param(
|
||||
[switch]$Force,
|
||||
[int]$MaxAttempts = 6,
|
||||
[int]$RetrySeconds = 10
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$logFile = 'C:\Logs\FilteredReportIP.log'
|
||||
New-Item -ItemType Directory -Path 'C:\Logs' -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
|
||||
function Log([string]$msg) {
|
||||
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||
"$ts $msg" | Tee-Object -FilePath $logFile -Append
|
||||
}
|
||||
|
||||
# Allowed ranges (CIDR -> first IP int + prefix bits)
|
||||
$allowedRanges = @(
|
||||
@{ Network = '10.134.48.0'; PrefixLen = 23 },
|
||||
@{ Network = '10.48.249.0'; PrefixLen = 26 }
|
||||
)
|
||||
|
||||
function ConvertTo-Uint32($ip) {
|
||||
$bytes = ([System.Net.IPAddress]::Parse($ip)).GetAddressBytes()
|
||||
[Array]::Reverse($bytes)
|
||||
return [BitConverter]::ToUInt32($bytes, 0)
|
||||
}
|
||||
|
||||
function Test-InAllowedRange([string]$ip) {
|
||||
try {
|
||||
$ipInt = ConvertTo-Uint32 $ip
|
||||
foreach ($r in $allowedRanges) {
|
||||
$netInt = ConvertTo-Uint32 $r.Network
|
||||
$mask = [uint32]([math]::Pow(2, 32) - [math]::Pow(2, 32 - $r.PrefixLen))
|
||||
if (($ipInt -band $mask) -eq ($netInt -band $mask)) { return $true }
|
||||
}
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
|
||||
Log "=== Filtered Report IP fire ==="
|
||||
$allIPs = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.IPAddress -notmatch '^169\.254' -and $_.IPAddress -ne '127.0.0.1' } |
|
||||
Select-Object -ExpandProperty IPAddress
|
||||
Log "All IPv4 addresses on this bay: $($allIPs -join ', ')"
|
||||
|
||||
$match = @($allIPs | Where-Object { Test-InAllowedRange $_ })
|
||||
if ($match.Count -eq 0) {
|
||||
Log "No IP in allowed ranges (10.134.48.0/23 or 10.48.249.0/26). Not POSTing."
|
||||
exit 2
|
||||
}
|
||||
$reportIP = $match[0]
|
||||
Log "Reporting IP: $reportIP"
|
||||
|
||||
$statusFile = "$env:ProgramData\GEA\FilteredReportIP\last-ip.txt"
|
||||
$lastIP = ''
|
||||
if (Test-Path $statusFile) { $lastIP = (Get-Content $statusFile -ErrorAction SilentlyContinue) -join '' }
|
||||
if (-not $Force -and $lastIP -eq $reportIP) {
|
||||
Log "IP unchanged from last POST ($lastIP). Skipping."
|
||||
exit 0
|
||||
}
|
||||
|
||||
$webhookUrl = 'https://tines.apps.geaerospace.com/webhook/aa891efac08bab3d077d9956ab6614d0/c687048cb0ee35c33760ad14cb9257eb'
|
||||
$body = @{
|
||||
host = $env:COMPUTERNAME
|
||||
fqdn = "$env:COMPUTERNAME.device.geaerospace.net"
|
||||
IP = $reportIP
|
||||
force_update = if ($Force) { 'True' } else { 'False' }
|
||||
}
|
||||
|
||||
for ($i = 1; $i -le $MaxAttempts; $i++) {
|
||||
try {
|
||||
Log "POST attempt $i/$MaxAttempts ..."
|
||||
$resp = Invoke-WebRequest -Uri $webhookUrl -Method POST -Body $body -TimeoutSec 60 -UseBasicParsing -ErrorAction Stop
|
||||
Log "POST succeeded. StatusCode=$($resp.StatusCode) Content=$($resp.Content)"
|
||||
New-Item -ItemType Directory -Path (Split-Path $statusFile) -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
Set-Content -Path $statusFile -Value $reportIP -Force
|
||||
exit 0
|
||||
} catch {
|
||||
Log "POST attempt $i failed: $_"
|
||||
if ($i -lt $MaxAttempts) { Start-Sleep -Seconds $RetrySeconds }
|
||||
}
|
||||
}
|
||||
Log "All $MaxAttempts attempts failed - giving up."
|
||||
exit 1
|
||||
@@ -1,68 +0,0 @@
|
||||
# Map-SfldShare.ps1 - Map S: drive on user logon using SFLD creds from HKLM.
|
||||
#
|
||||
# Runs as the interactive user (BUILTIN\Users, Limited) so the drive
|
||||
# mapping lands in the logged-in user's session. Reads username/password
|
||||
# directly from HKLM:\SOFTWARE\GE\SFLD\Credentials\* and passes them
|
||||
# inline to net use -- no Windows Credential Manager involvement.
|
||||
#
|
||||
# Why not the vendor's ConsumeCredentials.ps1: it calls
|
||||
# New-StoredCredential -Persist LocalMachine which requires admin.
|
||||
# ShopFloor is a non-admin user, so the cred-store step fails silently
|
||||
# and the subsequent net use (which relies on those stored creds) has
|
||||
# no authentication. Direct net use /user: bypasses the issue entirely.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path $logDir)) {
|
||||
try { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } catch { $logDir = $env:TEMP }
|
||||
}
|
||||
$logFile = Join-Path $logDir 'map-share.log'
|
||||
|
||||
function Write-MapLog {
|
||||
param([string]$Message)
|
||||
$line = '[{0}] [{1}] {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $env:USERNAME, $Message
|
||||
Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-MapLog '=== Map-SfldShare start ==='
|
||||
|
||||
$credsBase = 'HKLM:\SOFTWARE\GE\SFLD\Credentials'
|
||||
if (-not (Test-Path $credsBase)) {
|
||||
Write-MapLog 'No HKLM SFLD credentials yet - exiting'
|
||||
exit 0
|
||||
}
|
||||
|
||||
foreach ($entry in (Get-ChildItem -Path $credsBase -ErrorAction SilentlyContinue)) {
|
||||
$p = Get-ItemProperty -Path $entry.PSPath -ErrorAction SilentlyContinue
|
||||
if (-not $p -or -not $p.TargetHost -or -not $p.Username -or -not $p.Password) { continue }
|
||||
|
||||
$drive = $null
|
||||
$share = $null
|
||||
try { $drive = $p.DriveLetter } catch {}
|
||||
try { $share = $p.ShareName } catch {}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($drive) -or [string]::IsNullOrWhiteSpace($share)) { continue }
|
||||
|
||||
$drive = $drive.TrimEnd(':') + ':'
|
||||
$share = $share.TrimStart('\')
|
||||
$uncPath = "\\$($p.TargetHost)\$share"
|
||||
|
||||
# Skip if already mapped to the right target
|
||||
$existing = & net use $drive 2>&1
|
||||
if ($LASTEXITCODE -eq 0 -and ($existing -join "`n") -match [regex]::Escape($uncPath)) {
|
||||
Write-MapLog "$drive already mapped to $uncPath - skipping"
|
||||
continue
|
||||
}
|
||||
|
||||
& net use $drive /delete /y 2>$null | Out-Null
|
||||
$out = & net use $drive $uncPath /user:$($p.Username) $($p.Password) /persistent:yes 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-MapLog "Mapped $drive -> $uncPath"
|
||||
} else {
|
||||
Write-MapLog "FAILED $drive -> $uncPath (exit $LASTEXITCODE): $out"
|
||||
}
|
||||
}
|
||||
|
||||
Write-MapLog '=== Map-SfldShare end ==='
|
||||
exit 0
|
||||
@@ -251,14 +251,14 @@ function Get-Phase1 {
|
||||
# dashboard card flips to 100% green.
|
||||
|
||||
# Lockdown handover also ends the imaging autologon budget.
|
||||
# Run-ShopfloorSetup and Stage-Dispatcher top AutoLogonCount up to 10 for
|
||||
# SupportUser, and the Sysinternals Autologon run that switches the bay to
|
||||
# ShopFloor leaves it in place. ShopFloor then inherited the remainder and
|
||||
# Windows turned autologon off when it reached 0 - the bay parked at a
|
||||
# ShopFloor password prompt a few reboots after imaging. Delete it as soon
|
||||
# as Winlogon shows ShopFloor. Proven in the win11 VM 2026-10-02. GE-Enforce
|
||||
# common carries the same cleanup (Remove-AutoLogonCount.ps1) for bays
|
||||
# imaged before this.
|
||||
# Run-ShopfloorSetup tops AutoLogonCount up to 10 for SupportUser, and the
|
||||
# Sysinternals Autologon run that switches the bay to ShopFloor leaves it
|
||||
# in place. ShopFloor then inherited the remainder and Windows turned
|
||||
# autologon off when it reached 0 - the bay parked at a ShopFloor password
|
||||
# prompt a few reboots after imaging. Delete it as soon as Winlogon shows
|
||||
# ShopFloor. This task runs as Users (Limited) and the HKLM write can be
|
||||
# denied; the SYSTEM "GE Shopfloor Post-PPKG" task (Invoke-PostPpkg.ps1)
|
||||
# does the same cleanup at every startup until it has succeeded.
|
||||
try {
|
||||
$wlKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
|
||||
$wlNow = Get-ItemProperty $wlKey -ErrorAction Stop
|
||||
@@ -947,12 +947,9 @@ function Format-Snapshot {
|
||||
@{ Ok = $Snap.Phase3.InstallComplete; Failed = $false }
|
||||
)
|
||||
# Phase 4 (per-script Application Install) was retired 2026-04-28:
|
||||
# apps now deploy via the GE-Enforce manifest engine off
|
||||
# \\tsgwp00525\...\machineapps\, gated on AESFMA WiFi + S: mount
|
||||
# which both happen post-reboot. The monitor's responsibility ends
|
||||
# at "DSC bootstrap + creds + lockdown landed, time to reboot."
|
||||
# The manifest engine takes over from there on the next ShopFloor
|
||||
# logon.
|
||||
# site apps install from the post-PPKG list after the reboot
|
||||
# (Invoke-PostPpkg.ps1). The monitor's responsibility ends at
|
||||
# "DSC bootstrap + creds + lockdown landed, time to reboot."
|
||||
|
||||
$p4Done = ($Snap.Phase5.ConsumeCredsTask -and $Snap.Phase5.CredsPopulated)
|
||||
$p4Status = Get-PhaseStatus @(
|
||||
@@ -1250,7 +1247,7 @@ if ($AsTask -and (Test-Path -LiteralPath $syncCompleteMarker)) {
|
||||
# For these types, enrollment is "done" once Phase 1 (Identity) completes
|
||||
# and policies start arriving - there is no Phase 2-5 to wait for.
|
||||
$pcTypeFile = 'C:\Enrollment\pc-type.txt'
|
||||
$noDscTypes = @('Display', 'gea-shopfloor-display')
|
||||
$noDscTypes = @()
|
||||
$skipDsc = $false
|
||||
if (Test-Path $pcTypeFile) {
|
||||
$pcType = (Get-Content $pcTypeFile -First 1).Trim()
|
||||
@@ -1308,29 +1305,14 @@ try {
|
||||
$qrRefreshed = [bool]($qrText -notmatch 'not yet Azure AD joined')
|
||||
}
|
||||
|
||||
# Filtered Report IP shim - POSTs this bay's GE corp IP to the
|
||||
# Tines webhook directly with a tight subnet filter. Fires each
|
||||
# tick until it succeeds once (script's own status-file dedup
|
||||
# prevents duplicate POSTs). Bypasses the timing window where
|
||||
# GE's Intune-deployed Report IP misses (it may run before the
|
||||
# SCEP cert + AESFMA join land).
|
||||
if (-not $filteredReportIpSucceeded) {
|
||||
$filteredScript = Join-Path $PSScriptRoot 'Invoke-FilteredReportIP.ps1'
|
||||
if (Test-Path $filteredScript) {
|
||||
$rc = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $filteredScript 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) { $filteredReportIpSucceeded = $true }
|
||||
}
|
||||
}
|
||||
|
||||
Clear-Host
|
||||
Format-Snapshot -Snap $snap -LastSync $lastSync -NextRetrigger $nextRetrigger
|
||||
Write-Host ""
|
||||
Write-Host $qrText
|
||||
|
||||
# Final state: every phase landed. Apps are no longer in the gate -
|
||||
# they deploy via the GE-Enforce manifest engine post-reboot, gated
|
||||
# on AESFMA WiFi + S: drive mount which only happen after we hand
|
||||
# off to the ShopFloor autologon session. So this gate is now:
|
||||
# the site's post-PPKG list installs them after the reboot. So this
|
||||
# gate is now:
|
||||
# - CredsPopulated: SFLD share creds in HKLM (Machine-Enforce,
|
||||
# Acrobat-Enforce, CMM-Enforce, manifest
|
||||
# engine all need these)
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
# Restore-EDncReg.ps1 - Import a per-machine eDNC .reg backup from a folder.
|
||||
#
|
||||
# Two callers:
|
||||
# 1. Standard/03-RestoreEDncConfig.ps1 (imaging-time, source = local PXE copy)
|
||||
# 2. Shopfloor/lib/Update-MachineNumber.ps1 (post-image 9999 -> real, source
|
||||
# = tsgwp00525 SFLD share mounted via existing SFLD credential pattern)
|
||||
#
|
||||
# The .reg files live as <num>-<host>.reg or <num>.reg. Returns the path
|
||||
# that was imported, or $null if nothing was found/imported. Errors are
|
||||
# caught and logged via Write-Host (no throwing); callers continue.
|
||||
#
|
||||
# Dot-source with:
|
||||
# . "$PSScriptRoot\lib\Restore-EDncReg.ps1" (from Shopfloor\)
|
||||
# . "$PSScriptRoot\..\Shopfloor\lib\Restore-EDncReg.ps1" (from Standard\)
|
||||
|
||||
function Mount-SFLDShare {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Mounts a \\server\share path using SFLD creds from
|
||||
HKLM:\SOFTWARE\GE\SFLD\Credentials\*. Returns $true on success,
|
||||
$false if creds are missing or mount fails.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$SharePath,
|
||||
[string]$DriveLetter = 'V:'
|
||||
)
|
||||
|
||||
$server = ($SharePath -replace '^\\\\', '') -split '\\' | Select-Object -First 1
|
||||
$basePath = 'HKLM:\SOFTWARE\GE\SFLD\Credentials'
|
||||
if (-not (Test-Path $basePath)) { return $false }
|
||||
|
||||
$cred = $null
|
||||
foreach ($entry in Get-ChildItem -Path $basePath -ErrorAction SilentlyContinue) {
|
||||
$props = Get-ItemProperty -Path $entry.PSPath -ErrorAction SilentlyContinue
|
||||
if (-not $props -or -not $props.TargetHost) { continue }
|
||||
if ($props.TargetHost -eq $server -or
|
||||
$props.TargetHost -like "$server.*" -or
|
||||
$server -like "$($props.TargetHost).*") {
|
||||
$cred = $props; break
|
||||
}
|
||||
}
|
||||
if (-not $cred -or -not $cred.Username -or -not $cred.Password) { return $false }
|
||||
|
||||
& net use $DriveLetter /delete /y 2>$null | Out-Null
|
||||
$null = & net use $DriveLetter $SharePath /user:$($cred.Username) $($cred.Password) /persistent:no 2>&1
|
||||
return ($LASTEXITCODE -eq 0)
|
||||
}
|
||||
|
||||
function Import-EDncRegBackup {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Looks for <MachineNumber>-*.reg or <MachineNumber>.reg in SourceRoot
|
||||
and runs `reg import` on it. Caller is responsible for mounting any
|
||||
network share first; this function works on whatever local or drive-
|
||||
letter path is handed in.
|
||||
.PARAMETER SourceRoot
|
||||
Directory containing the .reg backups.
|
||||
.PARAMETER MachineNumber
|
||||
The target machine number (digits). Used as a filename prefix.
|
||||
.OUTPUTS
|
||||
Path of the imported file, or $null if none found / import failed.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$SourceRoot,
|
||||
[Parameter(Mandatory)][string]$MachineNumber
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $SourceRoot)) {
|
||||
Write-Host " Restore-EDncReg: source root not found: $SourceRoot"
|
||||
return $null
|
||||
}
|
||||
|
||||
$candidate = Get-ChildItem -Path $SourceRoot -Filter "$MachineNumber.reg" -File -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 1
|
||||
if (-not $candidate) {
|
||||
Write-Host " Restore-EDncReg: no backup for machine $MachineNumber in $SourceRoot"
|
||||
return $null
|
||||
}
|
||||
|
||||
Write-Host " Restore-EDncReg: importing $($candidate.FullName)"
|
||||
$out = & reg.exe import "$($candidate.FullName)" 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host " Restore-EDncReg: reg import failed (exit $LASTEXITCODE): $out"
|
||||
return $null
|
||||
}
|
||||
Write-Host " Restore-EDncReg: imported OK"
|
||||
return $candidate.FullName
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
# Set-OpenTextAutoStart.ps1 - place WJ Shopfloor.lnk in the All Users
|
||||
# Startup folder so HostExplorer's "WJ Shopfloor" session launches at
|
||||
# every login. Idempotent: re-running is a no-op when the .lnk already
|
||||
# exists at the same path.
|
||||
#
|
||||
# Used by per-pc-type 09-Setup scripts for shopfloor types whose only
|
||||
# business app is OpenText (common, waxtrace, genspect, heattreat).
|
||||
# collections + nocollections do NOT auto-start OpenText - their techs
|
||||
# pick which apps via Configure-PC.ps1.
|
||||
#
|
||||
# Source .lnk is created by the OpenText preinstall (Setup-OpenText.ps1)
|
||||
# on the public desktop. If the .lnk is missing, log a warning and exit
|
||||
# 0 - imaging chain still continues; auto-start can be re-attempted on a
|
||||
# subsequent login by re-running this script.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$startupDir = 'C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp'
|
||||
$publicDesktop = 'C:\Users\Public\Desktop'
|
||||
|
||||
$candidates = @(
|
||||
Join-Path $publicDesktop 'WJ Shopfloor.lnk'
|
||||
Join-Path (Join-Path $publicDesktop 'Shopfloor Tools') 'WJ Shopfloor.lnk'
|
||||
)
|
||||
$src = $candidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
||||
|
||||
if (-not $src) {
|
||||
Write-Warning "WJ Shopfloor.lnk not found on public desktop - OpenText auto-start NOT configured."
|
||||
Write-Warning " Searched: $($candidates -join ' ; ')"
|
||||
Write-Warning " Setup-OpenText.ps1 should create it during preinstall - check OpenText install state."
|
||||
return
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $startupDir)) {
|
||||
New-Item -Path $startupDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
$dst = Join-Path $startupDir 'WJ Shopfloor.lnk'
|
||||
try {
|
||||
Copy-Item -LiteralPath $src -Destination $dst -Force
|
||||
Write-Host "OpenText auto-start enabled: $dst (source: $src)"
|
||||
} catch {
|
||||
Write-Warning "Failed to copy WJ Shopfloor.lnk to startup: $_"
|
||||
}
|
||||
@@ -31,7 +31,7 @@ $ErrorActionPreference = 'Continue'
|
||||
# 1. -QRCoderDllPath if explicitly passed
|
||||
# 2. Next to this script ($PSScriptRoot\QRCoder.dll)
|
||||
# 3. Current working directory
|
||||
# 4. WJ canonical staging path (kept for cross-site portability)
|
||||
# 4. The standard staging path (C:\Enrollment\shopfloor-setup\Shopfloor)
|
||||
# ----------------------------------------------------------------------------
|
||||
function Resolve-QRCoderDll {
|
||||
param([string]$Override)
|
||||
|
||||
@@ -1,531 +0,0 @@
|
||||
# Update-MachineNumber.ps1 - Shared helper for reading and updating the
|
||||
# machine number in UDC and eDNC. Dot-source from any script that needs
|
||||
# machine-number operations:
|
||||
#
|
||||
# . "$PSScriptRoot\lib\Update-MachineNumber.ps1" (from Shopfloor\ scripts)
|
||||
# . "$PSScriptRoot\..\Shopfloor\lib\Update-MachineNumber.ps1" (from Standard\ scripts)
|
||||
# . "$PSScriptRoot\Update-MachineNumber.ps1" (from lib\ scripts)
|
||||
#
|
||||
# Exported functions:
|
||||
# Get-CurrentMachineNumber - returns @{ Udc = $string_or_null; Ednc = $string_or_null }
|
||||
# Update-MachineNumber - updates both, returns @{ UdcUpdated = $bool; EdncUpdated = $bool; Errors = @() }
|
||||
#
|
||||
# Both handle missing files/keys gracefully (app not installed = skip, not error).
|
||||
|
||||
$script:UdcSettingsPath = 'C:\ProgramData\UDC\udc_settings.json'
|
||||
$script:UdcExePath = 'C:\Program Files\UDC\UDC.exe'
|
||||
$script:EdncRegPath = 'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General'
|
||||
|
||||
function Get-CurrentMachineNumber {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Reads the current machine number from UDC settings JSON and eDNC registry.
|
||||
.OUTPUTS
|
||||
Hashtable with keys Udc ($string or $null) and Ednc ($string or $null).
|
||||
#>
|
||||
$result = @{ Udc = $null; Ednc = $null }
|
||||
|
||||
if (Test-Path $script:UdcSettingsPath) {
|
||||
try {
|
||||
$json = Get-Content $script:UdcSettingsPath -Raw | ConvertFrom-Json
|
||||
$result.Udc = $json.GeneralSettings.MachineNumber
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (Test-Path $script:EdncRegPath) {
|
||||
try {
|
||||
$result.Ednc = (Get-ItemProperty -Path $script:EdncRegPath -Name MachineNo -ErrorAction Stop).MachineNo
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
function Update-MTConnectVariantName {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Renames an MTConnect device across every file of a variant that carries it.
|
||||
|
||||
.DESCRIPTION
|
||||
Makino and Fanuc do not fit the one-file-one-service shape the other
|
||||
variants do.
|
||||
Per the vendor's "Installation Configuration and Trouble Shooting" guide,
|
||||
the device name lives in several files that must be identical or the agent
|
||||
may not start, and the Adapter Manager service rewrites the COMPLETE
|
||||
configuration when it stops - so an edit made while it runs is thrown away.
|
||||
|
||||
Makino, from a real bay capture:
|
||||
Agent\Devices.xml <Device name="7502" uuid="7502" id="MC41">
|
||||
Agent\Devices.EDM.xml <Device name="MAKINO-1" uuid="MAKINO-1" id="EDM41">
|
||||
Data\AdapterDataFile.xml <Name>MAKINO-1</Name>
|
||||
Agent\Agent.cfg Adapters { MAKINO-1 { Host=localhost ... } }
|
||||
|
||||
Fanuc, likewise:
|
||||
Agent\devices.xml <Device id="3101" name="3101" uuid="3101">
|
||||
Adapter\devices.xml the adapter's own generated copy
|
||||
Adapter\adapter.xml <DeviceName>/<DeviceID>/<DeviceUUID>3101
|
||||
|
||||
Editing only the agent's copy leaves the adapter streaming under the old
|
||||
identity. Seven of thirty-four captured Fanuc bays are in that state -
|
||||
agent on the right machine number, adapter still on the bay it was
|
||||
imaged from (4007 on a 7801 Toshulin, 3031 on a 7804).
|
||||
|
||||
The vendor's own path is ConfigurationManager.exe, a GUI tool, which a
|
||||
remote renumber cannot drive. This does what it does, in the required
|
||||
order: stop the manager and the agent, rewrite, start again.
|
||||
|
||||
Anything that cannot be renamed is reported rather than half-applied -
|
||||
a bay whose files disagree is worse than one still on its old name.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$NewNumber,
|
||||
[Parameter(Mandatory)][hashtable[]]$Targets,
|
||||
# Overridable so the rename can be exercised against a captured bay.
|
||||
[Parameter(Mandatory)][string]$Root,
|
||||
# Whatever owns the files goes down first - for Makino the Adapter
|
||||
# Manager rewrites its configuration from memory as it stops, and for
|
||||
# Fanuc the adapter holds its own copy. Overridable only so a test can
|
||||
# drive the stop/start ordering with real services.
|
||||
[string[]]$Services = @()
|
||||
)
|
||||
|
||||
$out = @{ Updated = @(); Errors = @() }
|
||||
|
||||
$present = @($Targets |
|
||||
ForEach-Object { @{ Path = (Join-Path $Root $_.Path); Kind = $_.Kind } } |
|
||||
Where-Object { Test-Path -LiteralPath $_.Path })
|
||||
if (-not $present) { return $out }
|
||||
|
||||
# Collect EVERY name in play, not just one. A bay half-renamed by the old
|
||||
# code carries two at once - Devices.xml already on the machine number while
|
||||
# the adapter files still say MAKINO-n - and replacing only the name we
|
||||
# happened to discover leaves the other behind, which is the very state this
|
||||
# is meant to end. Gather them all, then converge every file on the target.
|
||||
$names = New-Object System.Collections.Generic.HashSet[string]
|
||||
foreach ($t in $present) {
|
||||
$raw = Get-Content -LiteralPath $t.Path -Raw -ErrorAction SilentlyContinue
|
||||
if (-not $raw) { continue }
|
||||
switch ($t.Kind) {
|
||||
'attr' { if ($raw -match '<Device[^>]+name="([^"]+)"') { [void]$names.Add($matches[1]) } }
|
||||
'name' { if ($raw -match '<Name>\s*([^<]+?)\s*</Name>') { [void]$names.Add($matches[1]) } }
|
||||
'cfg' {
|
||||
# The adapter block's key is the bare line inside Adapters { }.
|
||||
if ($raw -match '(?ms)Adapters\s*\{\s*\r?\n\s*([^\s{]+)') { [void]$names.Add($matches[1]) }
|
||||
}
|
||||
}
|
||||
}
|
||||
[void]$names.Remove($NewNumber)
|
||||
if ($names.Count -eq 0) { return $out } # every file already on the target
|
||||
|
||||
# Agent is stopped too, so it re-reads Agent.cfg on the way back up.
|
||||
$stopped = @()
|
||||
foreach ($name in $Services) {
|
||||
$svc = Get-Service -Name $name -ErrorAction SilentlyContinue
|
||||
if (-not $svc) { continue }
|
||||
if ($svc.Status -ne 'Stopped') {
|
||||
try {
|
||||
Stop-Service -Name $name -Force -ErrorAction Stop
|
||||
$stopped += $name
|
||||
} catch {
|
||||
$out.Errors += "MTConnect: could not stop '$name' ($_); nothing was changed"
|
||||
foreach ($back in $stopped) {
|
||||
try { Start-Service -Name $back -ErrorAction Stop } catch {}
|
||||
}
|
||||
return $out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($t in $present) {
|
||||
try {
|
||||
$content = Get-Content -LiteralPath $t.Path -Raw -ErrorAction Stop
|
||||
$updated = $content
|
||||
$from = @()
|
||||
foreach ($old in $names) {
|
||||
$escaped = [regex]::Escape($old)
|
||||
switch ($t.Kind) {
|
||||
'attr' { $next = $updated -replace ('"' + $escaped + '"'), ('"' + $NewNumber + '"') }
|
||||
'name' { $next = $updated -replace ('<Name>\s*' + $escaped + '\s*</Name>'), ("<Name>$NewNumber</Name>") }
|
||||
# The adapter block is keyed by a bare name on its own line.
|
||||
'cfg' { $next = $updated -replace ('(?m)^(\s*)' + $escaped + '(\s*)$'), ("`${1}$NewNumber`${2}") }
|
||||
# <DeviceName>/<DeviceID>/<DeviceUUID> in the Fanuc adapter config
|
||||
'elem' { $next = $updated -replace ('(<Device(?:Name|ID|UUID)>)\s*' + $escaped + '\s*(</Device(?:Name|ID|UUID)>)'), ("`${1}$NewNumber`${2}") }
|
||||
}
|
||||
if ($next -ne $updated) { $from += $old; $updated = $next }
|
||||
}
|
||||
if ($updated -eq $content) { continue } # this file was already done
|
||||
Set-Content -LiteralPath $t.Path -Value $updated -NoNewline -ErrorAction Stop
|
||||
$out.Updated += "$($t.Path) ($($from -join ', ') -> $NewNumber)"
|
||||
} catch {
|
||||
$out.Errors += "MTConnect: rewrite failed for $($t.Path): $_"
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($name in $stopped) {
|
||||
try { Start-Service -Name $name -ErrorAction Stop }
|
||||
catch { $out.Errors += "MTConnect: restart of '$name' failed: $_" }
|
||||
}
|
||||
|
||||
return $out
|
||||
}
|
||||
|
||||
function Update-MachineNumber {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Updates UDC + eDNC machine number in one step. Stops UDC before writing,
|
||||
relaunches after.
|
||||
.PARAMETER NewNumber
|
||||
The new machine number (digits only - caller validates).
|
||||
.PARAMETER Site
|
||||
Site name passed to UDC.exe -site argument. Defaults to 'West Jefferson'.
|
||||
.OUTPUTS
|
||||
Hashtable: @{ UdcUpdated = $bool; EdncUpdated = $bool; Errors = @() }
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$NewNumber,
|
||||
|
||||
[string]$Site = 'West Jefferson'
|
||||
)
|
||||
|
||||
$out = @{ UdcUpdated = $false; EdncUpdated = $false; Errors = @(); RegImported = $null; OldUdc = $null; OldEdnc = $null }
|
||||
|
||||
# If the machine number is changing (placeholder->real OR real->real
|
||||
# reassignment after a duplicate-image), pull per-machine state for the
|
||||
# new number from the SFLD share: NTLARS .reg, UDC settings, UDC live
|
||||
# data. The live-data restore is idempotent via one-shot migrated/
|
||||
# consumption, so it stays safe on reassign too.
|
||||
$current = Get-CurrentMachineNumber
|
||||
$out.OldUdc = $current.Udc
|
||||
$out.OldEdnc = $current.Ednc
|
||||
$isChanging = ($current.Udc -ne $NewNumber) -or ($current.Ednc -ne $NewNumber)
|
||||
|
||||
if ($isChanging -and $NewNumber -ne '9999') {
|
||||
$sharePath = $null
|
||||
$siteCfgPath = 'C:\Enrollment\site-config.json'
|
||||
if (Test-Path $siteCfgPath) {
|
||||
try {
|
||||
$cfg = Get-Content $siteCfgPath -Raw | ConvertFrom-Json
|
||||
# Alias-aware lookup: prefer new key, fall back to legacy.
|
||||
# PowerShell 5.1 has no null-coalesce operator.
|
||||
$sharePath = $cfg.pcProfiles.'gea-shopfloor-collections'.ntlarsBackupSharePath
|
||||
if (-not $sharePath) { $sharePath = $cfg.pcProfiles.'Standard-Machine'.ntlarsBackupSharePath }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if ($sharePath) {
|
||||
try {
|
||||
. (Join-Path $PSScriptRoot 'Restore-EDncReg.ps1')
|
||||
$mounted = Mount-SFLDShare -SharePath $sharePath -DriveLetter 'V:'
|
||||
if ($mounted) {
|
||||
try {
|
||||
$out.RegImported = Import-EDncRegBackup -SourceRoot 'V:\' -MachineNumber $NewNumber
|
||||
} finally {
|
||||
& net use V: /delete /y 2>$null | Out-Null
|
||||
}
|
||||
} else {
|
||||
Write-Host " Update-MachineNumber: SFLD share unreachable - skipping restore."
|
||||
}
|
||||
} catch {
|
||||
$out.Errors += "ntlars restore failed: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# --- UDC settings JSON restore: pull udc_settings_<NewNumber>.json
|
||||
# from the SFLD UDC settings_backups share. At imaging time
|
||||
# 00-PreInstall-MachineApps.ps1 pulls this from the local
|
||||
# C:\Enrollment mirror, but a 9999-placeholder PC has no real
|
||||
# pre-stage. Once the tech sets the real number we have SFLD
|
||||
# creds, so go direct to the canonical share. ---
|
||||
$udcSettingsSharePath = $null
|
||||
if ($cfg) {
|
||||
try {
|
||||
$udcSettingsSharePath = $cfg.pcProfiles.'gea-shopfloor-collections'.udcSettingsSharePath
|
||||
if (-not $udcSettingsSharePath) { $udcSettingsSharePath = $cfg.pcProfiles.'Standard-Machine'.udcSettingsSharePath }
|
||||
} catch {}
|
||||
}
|
||||
if ($udcSettingsSharePath) {
|
||||
try {
|
||||
$mountedUdcSet = Mount-SFLDShare -SharePath $udcSettingsSharePath -DriveLetter 'X:'
|
||||
if ($mountedUdcSet) {
|
||||
try {
|
||||
$srcSettings = Join-Path 'X:\' "udc_settings_$NewNumber.json"
|
||||
if (Test-Path -LiteralPath $srcSettings) {
|
||||
Get-Process UDC -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
try { $_.Kill(); $_.WaitForExit(5000) | Out-Null } catch {}
|
||||
}
|
||||
Start-Sleep -Milliseconds 500
|
||||
$localUdcDir = 'C:\ProgramData\UDC'
|
||||
if (-not (Test-Path $localUdcDir)) { New-Item -ItemType Directory -Path $localUdcDir -Force | Out-Null }
|
||||
Copy-Item -LiteralPath $srcSettings -Destination $script:UdcSettingsPath -Force -ErrorAction Stop
|
||||
$out.UdcSettingsRestored = $true
|
||||
Write-Host " Update-MachineNumber: UDC settings restored from $srcSettings"
|
||||
} else {
|
||||
Write-Host " Update-MachineNumber: no udc_settings_$NewNumber.json on settings_backups share"
|
||||
}
|
||||
} finally {
|
||||
& net use X: /delete /y 2>$null | Out-Null
|
||||
}
|
||||
} else {
|
||||
Write-Host " Update-MachineNumber: UDC settings_backups share unreachable - skipping settings restore."
|
||||
}
|
||||
} catch {
|
||||
$out.Errors += "UDC settings restore failed: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# --- UDC data restore: pull CurrentData.json + ArchivedData/ from
|
||||
# the per-bay backup at <udcBackupSharePath>\<NewNumber>\.
|
||||
# One-shot: after successful restore, the live backup at the root
|
||||
# is moved into <NewNumber>\migrated\<timestamp>\ so it can't be
|
||||
# replayed on subsequent reboots or reused for a future PC at the
|
||||
# same bay. The 'isPlaceholder' guard above ensures this whole
|
||||
# block only ever fires once per PC's lifetime (placeholder->real
|
||||
# transition). ---
|
||||
$udcSharePath = $null
|
||||
if ($cfg) {
|
||||
try {
|
||||
$udcSharePath = $cfg.pcProfiles.'gea-shopfloor-collections'.udcBackupSharePath
|
||||
if (-not $udcSharePath) { $udcSharePath = $cfg.pcProfiles.'Standard-Machine'.udcBackupSharePath }
|
||||
} catch {}
|
||||
}
|
||||
if ($udcSharePath) {
|
||||
try {
|
||||
# N:, never W:. GE-Enforce owns W: for its whole cycle, and this
|
||||
# runs as SYSTEM in the same drive namespace - taking W: here kills
|
||||
# the share out from under an in-flight enforce cycle.
|
||||
$mountedUdc = Mount-SFLDShare -SharePath $udcSharePath -DriveLetter 'N:'
|
||||
if ($mountedUdc) {
|
||||
try {
|
||||
$bayDir = Join-Path 'N:\' $NewNumber
|
||||
$srcCur = Join-Path $bayDir 'CurrentData.json'
|
||||
$srcArc = Join-Path $bayDir 'ArchivedData'
|
||||
if (Test-Path -LiteralPath $srcCur) {
|
||||
Write-Host " Update-MachineNumber: UDC backup found at $bayDir - restoring."
|
||||
|
||||
# Stop UDC pre-emptively so CurrentData.json isn't locked
|
||||
Get-Process UDC -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
try { $_.Kill(); $_.WaitForExit(5000) | Out-Null } catch {}
|
||||
}
|
||||
Start-Sleep -Milliseconds 500
|
||||
|
||||
$localUdcDir = 'C:\ProgramData\UDC'
|
||||
if (-not (Test-Path $localUdcDir)) { New-Item -ItemType Directory -Path $localUdcDir -Force | Out-Null }
|
||||
|
||||
$localArc = Join-Path $localUdcDir 'ArchivedData'
|
||||
|
||||
# Copy
|
||||
Copy-Item -LiteralPath $srcCur -Destination (Join-Path $localUdcDir 'CurrentData.json') -Force -ErrorAction Stop
|
||||
if (Test-Path -LiteralPath $srcArc) {
|
||||
if (Test-Path -LiteralPath $localArc) { Remove-Item -LiteralPath $localArc -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
Copy-Item -LiteralPath $srcArc -Destination $localArc -Recurse -Force -ErrorAction Stop
|
||||
}
|
||||
|
||||
# Move live backup -> migrated/<timestamp>/ (one-shot consumption)
|
||||
$stamp = (Get-Date -Format 'yyyy-MM-ddTHH-mm-ssZ')
|
||||
$migDir = Join-Path $bayDir 'migrated'
|
||||
$migStamp = Join-Path $migDir $stamp
|
||||
if (-not (Test-Path -LiteralPath $migDir)) { New-Item -ItemType Directory -Path $migDir -Force | Out-Null }
|
||||
if (-not (Test-Path -LiteralPath $migStamp)) { New-Item -ItemType Directory -Path $migStamp -Force | Out-Null }
|
||||
|
||||
Move-Item -LiteralPath $srcCur -Destination (Join-Path $migStamp 'CurrentData.json') -Force -ErrorAction Stop
|
||||
if (Test-Path -LiteralPath $srcArc) {
|
||||
Move-Item -LiteralPath $srcArc -Destination (Join-Path $migStamp 'ArchivedData') -Force -ErrorAction Stop
|
||||
}
|
||||
$bakManifest = Join-Path $bayDir 'backup.manifest.json'
|
||||
if (Test-Path -LiteralPath $bakManifest) {
|
||||
Move-Item -LiteralPath $bakManifest -Destination (Join-Path $migStamp 'backup.manifest.json') -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Audit manifest in migrated/<stamp>/
|
||||
$localArcInfo = if (Test-Path $localArc) { Get-ChildItem $localArc -Recurse -File -ErrorAction SilentlyContinue } else { @() }
|
||||
$restoreManifest = [ordered]@{
|
||||
RestoredAt = (Get-Date -Format 'o')
|
||||
DestinationHostname = $env:COMPUTERNAME
|
||||
DestinationUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
MachineNumber = $NewNumber
|
||||
CurrentDataBytes = (Get-Item (Join-Path $localUdcDir 'CurrentData.json')).Length
|
||||
ArchivedDataFiles = $localArcInfo.Count
|
||||
ArchivedDataBytes = ($localArcInfo | Measure-Object Length -Sum).Sum
|
||||
RestoredVia = 'Update-MachineNumber.ps1'
|
||||
}
|
||||
$restoreManifest | ConvertTo-Json | Set-Content -Path (Join-Path $migStamp 'restore.manifest.json') -Encoding UTF8
|
||||
|
||||
$out.UdcRestored = $true
|
||||
Write-Host " Update-MachineNumber: UDC restore OK (consumed -> migrated\$stamp\)"
|
||||
} else {
|
||||
Write-Host " Update-MachineNumber: no UDC backup at $bayDir (fresh PC, no prior data)"
|
||||
}
|
||||
} finally {
|
||||
& net use N: /delete /y 2>$null | Out-Null
|
||||
}
|
||||
} else {
|
||||
Write-Host " Update-MachineNumber: UDC backup share unreachable - skipping UDC restore."
|
||||
}
|
||||
} catch {
|
||||
$out.Errors += "UDC restore failed: $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Stop UDC before editing its JSON (avoid stale shutdown write) ---
|
||||
Get-Process UDC -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
try { $_.Kill(); $_.WaitForExit(5000) | Out-Null } catch {}
|
||||
}
|
||||
Start-Sleep -Seconds 1
|
||||
|
||||
# --- Update UDC settings JSON ---
|
||||
# -ErrorAction Stop on the WRITE so PermissionDenied / IO errors become
|
||||
# terminating and actually hit the catch block. Without this, the cmdlet
|
||||
# writes a non-terminating error (visible in transcript) but flow
|
||||
# continues + $out.UdcUpdated is set to $true, leading the dialog to
|
||||
# report "UDC updated" when the file write actually failed.
|
||||
if (Test-Path $script:UdcSettingsPath) {
|
||||
try {
|
||||
$json = Get-Content $script:UdcSettingsPath -Raw -ErrorAction Stop | ConvertFrom-Json
|
||||
$json.GeneralSettings.MachineNumber = $NewNumber
|
||||
$json | ConvertTo-Json -Depth 99 | Set-Content -Path $script:UdcSettingsPath -Encoding UTF8 -ErrorAction Stop
|
||||
$out.UdcUpdated = $true
|
||||
} catch {
|
||||
$out.Errors += "UDC update failed: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Update eDNC registry ---
|
||||
# Same -ErrorAction Stop reasoning as above. Set-ItemProperty's
|
||||
# PermissionDenied is non-terminating by default; without -ErrorAction
|
||||
# Stop, the catch block never fires and $out.EdncUpdated=$true gets set
|
||||
# despite the write failing. This is the bug that made the 13:35:39
|
||||
# tech run on FGY07FZ3 report "eDNC updated to 3005 / All updates
|
||||
# succeeded" while the actual reg value stayed at 9999.
|
||||
if (Test-Path $script:EdncRegPath) {
|
||||
try {
|
||||
Set-ItemProperty -Path $script:EdncRegPath -Name MachineNo -Value $NewNumber -Type String -Force -ErrorAction Stop
|
||||
$out.EdncUpdated = $true
|
||||
} catch {
|
||||
$out.Errors += "eDNC update failed: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Relaunch UDC with new args ---
|
||||
if ((Test-Path $script:UdcExePath) -and $out.UdcUpdated) {
|
||||
try {
|
||||
# UDC.exe arg signature: quoted site name (with space), then
|
||||
# dash-prefixed machine number. Example: UDC.exe "West Jefferson" -7605
|
||||
Start-Process -FilePath $script:UdcExePath -ArgumentList @("`"$Site`"", "-$NewNumber")
|
||||
} catch {
|
||||
$out.Errors += "UDC relaunch failed: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Update MTConnect Devices.xml (per-variant) + restart agent services ---
|
||||
# MTConnect deploys via the GE-Enforce manifest engine + the
|
||||
# Install_MTConnect_ExisDir_BatConvert.ps1 wrapper. Devices.xml on disk
|
||||
# has the machine number embedded in the <Device name= uuid= id=> attrs.
|
||||
# When the tech changes 9999 -> real number, those attrs need to follow,
|
||||
# OR MTConnect data will keep reporting under the old/wrong identity.
|
||||
#
|
||||
# We do an inline substitution rather than re-running the full wrapper
|
||||
# because the tech may not have credential delegation to the SFLD share
|
||||
# from their interactive session.
|
||||
$out.MTConnectUpdated = @()
|
||||
# Drive this off "is the service installed?" rather than file existence:
|
||||
# Fanuc and Okuma both deploy to C:\MTConnect\Agent\, distinguishable only
|
||||
# by the registered service name (NTFS is case-insensitive so the two
|
||||
# devices.xml / Devices.xml entries collapse to the same file). Without
|
||||
# this filter, the Okuma branch on an Okuma PC sees the file already
|
||||
# rewritten by the (no-op) Fanuc branch and skips the service restart.
|
||||
# Okuma and eDNC keep the name in one file only - verified across 7 Okuma
|
||||
# and 15 eDNC bay captures - so the simple edit is right for them. Fanuc and
|
||||
# Makino are handled below instead.
|
||||
$mtcVariants = @(
|
||||
@{ Service='MTConnect Agent Okuma'; Path='C:\MTConnect\Agent\Devices.xml' },
|
||||
@{ Service='MTConnect eDNC Agent'; Path='C:\MTConnect_eDNC\Agent\Devices.xml' }
|
||||
)
|
||||
foreach ($v in $mtcVariants) {
|
||||
$svc = Get-Service -Name $v.Service -ErrorAction SilentlyContinue
|
||||
if (-not $svc) { continue }
|
||||
if (-not (Test-Path -LiteralPath $v.Path)) { continue }
|
||||
try {
|
||||
$content = Get-Content -LiteralPath $v.Path -Raw -ErrorAction Stop
|
||||
# Find the Device root's name= attr to discover the OLD identifier
|
||||
if ($content -notmatch '<Device[^>]+name="([^"]+)"') { continue }
|
||||
$oldName = $matches[1]
|
||||
# Most variants encode machine number as the trailing digit run:
|
||||
# Fanuc/Makino: name="9999" -> trailing 9999
|
||||
# OKUMA: name="loc9999" -> trailing 9999, prefix 'loc'
|
||||
# eDNC: name="eDNC_OKUMA9999" -> trailing 9999, prefix 'eDNC_OKUMA'
|
||||
if ($oldName -notmatch '^(.*?)(\d+)$') { continue }
|
||||
$prefix = $matches[1]
|
||||
$oldDigits = $matches[2]
|
||||
if ($oldDigits -eq $NewNumber) { continue } # already correct
|
||||
$oldFull = "$prefix$oldDigits"
|
||||
$newFull = "$prefix$NewNumber"
|
||||
$oldEsc = [regex]::Escape($oldFull)
|
||||
# Quoted attr value: name="X" / uuid="X" / id="X"
|
||||
$content = $content -replace ('"' + $oldEsc + '"'), ('"' + $newFull + '"')
|
||||
# OKUMA-style with serial after dot: uuid="locX.<serial>"
|
||||
$content = $content -replace ('"' + $oldEsc + '\.'), ('"' + $newFull + '.')
|
||||
Set-Content -LiteralPath $v.Path -Value $content -NoNewline -ErrorAction Stop
|
||||
$out.MTConnectUpdated += "$($v.Path) ($oldFull -> $newFull)"
|
||||
try { Restart-Service -Name $v.Service -Force -ErrorAction Stop }
|
||||
catch { $out.Errors += "Restart $($v.Service) failed: $_" }
|
||||
} catch {
|
||||
$out.Errors += "MTConnect Devices.xml update failed for $($v.Path): $_"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Multi-file variants: rename every copy, services stopped first ---
|
||||
# Fanuc and Makino each keep the device name in more than one file, and the
|
||||
# agent's copy alone is not enough - the adapter goes on streaming under the
|
||||
# old identity, which is how seven captured Fanuc bays ended up with the
|
||||
# agent on the right machine number and the adapter on the bay it was
|
||||
# imaged from.
|
||||
$multiFileVariants = @(
|
||||
@{
|
||||
Probe = 'MTConnect Agent Fanuc'
|
||||
Root = 'C:\MTConnect'
|
||||
Services = @('MTConnect Adapter Fanuc', 'MTConnect Agent Fanuc')
|
||||
Targets = @(
|
||||
@{ Path = 'Agent\devices.xml'; Kind = 'attr' },
|
||||
@{ Path = 'Adapter\devices.xml'; Kind = 'attr' },
|
||||
@{ Path = 'Adapter\adapter.xml'; Kind = 'elem' }
|
||||
)
|
||||
},
|
||||
@{
|
||||
Probe = 'Makino MTConnect Agent'
|
||||
Root = 'C:\Makino-MTConnect'
|
||||
Services = @('Makino Adapter Manager', 'Makino MTConnect Agent')
|
||||
Targets = @(
|
||||
@{ Path = 'Agent\Devices.xml'; Kind = 'attr' },
|
||||
@{ Path = 'Agent\Devices.EDM.xml'; Kind = 'attr' },
|
||||
@{ Path = 'Data\AdapterDataFile.xml'; Kind = 'name' },
|
||||
@{ Path = 'Agent\Agent.cfg'; Kind = 'cfg' }
|
||||
)
|
||||
}
|
||||
)
|
||||
foreach ($variant in $multiFileVariants) {
|
||||
if (-not (Get-Service -Name $variant.Probe -ErrorAction SilentlyContinue)) { continue }
|
||||
$renamed = Update-MTConnectVariantName -NewNumber $NewNumber `
|
||||
-Root $variant.Root -Targets $variant.Targets -Services $variant.Services
|
||||
$out.MTConnectUpdated += $renamed.Updated
|
||||
$out.Errors += $renamed.Errors
|
||||
}
|
||||
|
||||
# Keep C:\Enrollment\machine-number.txt in sync. Post-imaging GE-Enforce
|
||||
# prefers eDNC reg, but imaging-time scripts (Install-FromManifest
|
||||
# TargetMachineNumbers filter, 01-eDNC.ps1, 03-RestoreEDncConfig.ps1)
|
||||
# still read this file. Avoid drift on reassign.
|
||||
try {
|
||||
$mnFile = 'C:\Enrollment\machine-number.txt'
|
||||
$mnDir = Split-Path -Parent $mnFile
|
||||
if (-not (Test-Path $mnDir)) { New-Item -ItemType Directory -Path $mnDir -Force | Out-Null }
|
||||
Set-Content -Path $mnFile -Value $NewNumber -Encoding UTF8 -NoNewline
|
||||
$out.MachineNumberTxtUpdated = $true
|
||||
} catch {
|
||||
$out.Errors += "machine-number.txt update failed: $_"
|
||||
}
|
||||
|
||||
return $out
|
||||
}
|
||||
Reference in New Issue
Block a user