Shopfloor PC type system, webapp enhancements, slim Blancco GRUB
- Shopfloor PC type menu (CMM, WaxAndTrace, Keyence, Genspect, Display, Standard) - Baseline scripts: OpenText CSF, Start Menu shortcuts, network/WinRM, power/display - Standard type: eDNC + MarkZebra with 64-bit path mirroring - CMM type: Hexagon CLM Tools, PC-DMIS 2016/2019 R2 - Display sub-type: Lobby vs Dashboard - Webapp: enrollment management, image config editor, UI refresh - Upload-Image.ps1: robocopy MCL cache to PXE server - Download-Drivers.ps1: Dell driver download pipeline - Slim Blancco GRUB EFI (10MB -> 660KB) for old hardware compat - Shopfloor display imaging guide docs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
230
Download-Drivers.ps1
Normal file
230
Download-Drivers.ps1
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
#
|
||||||
|
# Download-Drivers.ps1 — Download selected hardware drivers from GE CDN
|
||||||
|
#
|
||||||
|
# Reads user_selections.json and HardwareDriver.json from the MCL cache
|
||||||
|
# to download only the driver packs for your selected hardware models.
|
||||||
|
# Bypasses Media Creator Lite's unreliable download mechanism.
|
||||||
|
#
|
||||||
|
# Downloads go into the MCL cache structure so Upload-Image.ps1 can
|
||||||
|
# upload them with -IncludeDrivers.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# .\Download-Drivers.ps1 (download all selected models)
|
||||||
|
# .\Download-Drivers.ps1 -ListOnly (show what would be downloaded)
|
||||||
|
# .\Download-Drivers.ps1 -CachePath "D:\MCL\Cache" (custom cache location)
|
||||||
|
# .\Download-Drivers.ps1 -Force (re-download even if already cached)
|
||||||
|
#
|
||||||
|
# Requires internet access. Run on the workstation, not the PXE server.
|
||||||
|
#
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$CachePath = "C:\ProgramData\GEAerospace\MediaCreator\Cache",
|
||||||
|
[switch]$ListOnly,
|
||||||
|
[switch]$Force
|
||||||
|
)
|
||||||
|
|
||||||
|
function Format-Size {
|
||||||
|
param([long]$Bytes)
|
||||||
|
if ($Bytes -ge 1GB) { return "{0:N1} GB" -f ($Bytes / 1GB) }
|
||||||
|
if ($Bytes -ge 1MB) { return "{0:N1} MB" -f ($Bytes / 1MB) }
|
||||||
|
return "{0:N0} KB" -f ($Bytes / 1KB)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-DestDir {
|
||||||
|
param([string]$Dir)
|
||||||
|
return ($Dir -replace '^\*destinationdir\*\\?', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " PXE Driver Downloader" -ForegroundColor Cyan
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# --- Validate paths ---
|
||||||
|
$DeployPath = Join-Path $CachePath "Deploy"
|
||||||
|
$ControlPath = Join-Path $DeployPath "Control"
|
||||||
|
$ToolsPath = Join-Path (Split-Path $CachePath -Parent) "Tools"
|
||||||
|
if (-not (Test-Path $ToolsPath -PathType Container)) {
|
||||||
|
$ToolsPath = "C:\ProgramData\GEAerospace\MediaCreator\Tools"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $ControlPath -PathType Container)) {
|
||||||
|
Write-Host "ERROR: Deploy\Control not found at $ControlPath" -ForegroundColor Red
|
||||||
|
Write-Host " Run Media Creator Lite first to cache the base content." -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Parse user_selections.json ---
|
||||||
|
$SelectionsFile = Join-Path $ToolsPath "user_selections.json"
|
||||||
|
if (-not (Test-Path $SelectionsFile)) {
|
||||||
|
Write-Host "ERROR: user_selections.json not found at $SelectionsFile" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$selections = (Get-Content $SelectionsFile -Raw | ConvertFrom-Json)[0]
|
||||||
|
$selectedOsId = $selections.OperatingSystemSelection
|
||||||
|
$selectedModelIds = @($selections.HardwareModelSelection | ForEach-Object { $_.Id } | Select-Object -Unique)
|
||||||
|
|
||||||
|
# --- Parse HardwareDriver.json ---
|
||||||
|
$driverJsonFile = Join-Path $ControlPath "HardwareDriver.json"
|
||||||
|
if (-not (Test-Path $driverJsonFile)) {
|
||||||
|
Write-Host "ERROR: HardwareDriver.json not found in $ControlPath" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$driverJson = Get-Content $driverJsonFile -Raw | ConvertFrom-Json
|
||||||
|
|
||||||
|
# --- Match drivers to selections ---
|
||||||
|
$matchedDrivers = @($driverJson | Where-Object {
|
||||||
|
$selectedModelIds -contains $_.family -and $_.aOsIds -contains $selectedOsId
|
||||||
|
})
|
||||||
|
|
||||||
|
# Deduplicate by DestinationDir (some models share a driver pack)
|
||||||
|
$uniqueDrivers = [ordered]@{}
|
||||||
|
foreach ($drv in $matchedDrivers) {
|
||||||
|
$rel = Resolve-DestDir $drv.DestinationDir
|
||||||
|
if (-not $uniqueDrivers.Contains($rel)) {
|
||||||
|
$uniqueDrivers[$rel] = $drv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalSize = [long]0
|
||||||
|
$uniqueDrivers.Values | ForEach-Object { $totalSize += $_.size }
|
||||||
|
|
||||||
|
# --- Display plan ---
|
||||||
|
Write-Host " Cache: $CachePath"
|
||||||
|
Write-Host " OS ID: $selectedOsId"
|
||||||
|
Write-Host " Models: $($selectedModelIds.Count) selected"
|
||||||
|
Write-Host " Drivers: $($uniqueDrivers.Count) unique pack(s) ($(Format-Size $totalSize))" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
if ($uniqueDrivers.Count -eq 0) {
|
||||||
|
Write-Host "No drivers match your selections." -ForegroundColor Yellow
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show each driver pack
|
||||||
|
$idx = 0
|
||||||
|
foreach ($rel in $uniqueDrivers.Keys) {
|
||||||
|
$idx++
|
||||||
|
$drv = $uniqueDrivers[$rel]
|
||||||
|
$localDir = Join-Path $CachePath $rel
|
||||||
|
$cached = Test-Path $localDir -PathType Container
|
||||||
|
$status = if ($cached -and -not $Force) { "[CACHED]" } else { "[DOWNLOAD]" }
|
||||||
|
$color = if ($cached -and -not $Force) { "Green" } else { "Yellow" }
|
||||||
|
|
||||||
|
Write-Host (" {0,3}. {1,-12} {2} ({3})" -f $idx, $status, $drv.modelsfriendlyname, (Format-Size $drv.size)) -ForegroundColor $color
|
||||||
|
Write-Host " $($drv.FileName)" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
if ($ListOnly) {
|
||||||
|
Write-Host " (list only — run without -ListOnly to download)" -ForegroundColor Gray
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Download and extract ---
|
||||||
|
$downloadDir = Join-Path $env:TEMP "PXE-DriverDownloads"
|
||||||
|
if (-not (Test-Path $downloadDir)) { New-Item -ItemType Directory -Path $downloadDir -Force | Out-Null }
|
||||||
|
|
||||||
|
$completed = 0
|
||||||
|
$skipped = 0
|
||||||
|
$errors = 0
|
||||||
|
|
||||||
|
foreach ($rel in $uniqueDrivers.Keys) {
|
||||||
|
$drv = $uniqueDrivers[$rel]
|
||||||
|
$localDir = Join-Path $CachePath $rel
|
||||||
|
$zipFile = Join-Path $downloadDir $drv.FileName
|
||||||
|
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host "[$($completed + $skipped + $errors + 1)/$($uniqueDrivers.Count)] $($drv.modelsfriendlyname)" -ForegroundColor Cyan
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Skip if already cached (unless -Force)
|
||||||
|
if ((Test-Path $localDir -PathType Container) -and -not $Force) {
|
||||||
|
Write-Host " Already cached at $rel" -ForegroundColor Green
|
||||||
|
$skipped++
|
||||||
|
Write-Host ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Download
|
||||||
|
Write-Host " Downloading $(Format-Size $drv.size) ..." -ForegroundColor Gray
|
||||||
|
Write-Host " URL: $($drv.url)" -ForegroundColor DarkGray
|
||||||
|
|
||||||
|
try {
|
||||||
|
# Use curl.exe for progress display on large files
|
||||||
|
if (Get-Command curl.exe -ErrorAction SilentlyContinue) {
|
||||||
|
& curl.exe -L -o $zipFile $drv.url --progress-bar --fail
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "curl failed with exit code $LASTEXITCODE" }
|
||||||
|
} else {
|
||||||
|
# Fallback: WebClient (streams to disk, no buffering)
|
||||||
|
$wc = New-Object System.Net.WebClient
|
||||||
|
$wc.DownloadFile($drv.url, $zipFile)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host " ERROR: Download failed - $_" -ForegroundColor Red
|
||||||
|
$errors++
|
||||||
|
Write-Host ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify SHA256 hash
|
||||||
|
Write-Host " Verifying SHA256 hash ..." -ForegroundColor Gray
|
||||||
|
$actualHash = (Get-FileHash -Path $zipFile -Algorithm SHA256).Hash
|
||||||
|
if ($actualHash -ne $drv.hash) {
|
||||||
|
Write-Host " ERROR: Hash mismatch!" -ForegroundColor Red
|
||||||
|
Write-Host " Expected: $($drv.hash)" -ForegroundColor Red
|
||||||
|
Write-Host " Got: $actualHash" -ForegroundColor Red
|
||||||
|
Remove-Item -Path $zipFile -Force -ErrorAction SilentlyContinue
|
||||||
|
$errors++
|
||||||
|
Write-Host ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
Write-Host " Hash OK." -ForegroundColor Green
|
||||||
|
|
||||||
|
# Extract to cache destination
|
||||||
|
Write-Host " Extracting to $rel ..." -ForegroundColor Gray
|
||||||
|
if (Test-Path $localDir) { Remove-Item -Recurse -Force $localDir -ErrorAction SilentlyContinue }
|
||||||
|
New-Item -ItemType Directory -Path $localDir -Force | Out-Null
|
||||||
|
|
||||||
|
try {
|
||||||
|
Expand-Archive -Path $zipFile -DestinationPath $localDir -Force
|
||||||
|
} catch {
|
||||||
|
Write-Host " ERROR: Extraction failed - $_" -ForegroundColor Red
|
||||||
|
$errors++
|
||||||
|
Write-Host ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clean up zip
|
||||||
|
Remove-Item -Path $zipFile -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
Write-Host " Done." -ForegroundColor Green
|
||||||
|
$completed++
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Summary ---
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " Download Summary" -ForegroundColor Cyan
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " Downloaded: $completed" -ForegroundColor Green
|
||||||
|
if ($skipped -gt 0) { Write-Host " Skipped: $skipped (already cached)" -ForegroundColor Gray }
|
||||||
|
if ($errors -gt 0) { Write-Host " Failed: $errors" -ForegroundColor Red }
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
if ($completed -gt 0 -or $skipped -gt 0) {
|
||||||
|
Write-Host "Driver packs are in the MCL cache at:" -ForegroundColor Cyan
|
||||||
|
Write-Host " $DeployPath\Out-of-box Drivers\" -ForegroundColor White
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "To upload to the PXE server:" -ForegroundColor Cyan
|
||||||
|
Write-Host " .\Upload-Image.ps1 -IncludeDrivers" -ForegroundColor White
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clean up temp dir if empty
|
||||||
|
if ((Get-ChildItem $downloadDir -Force -ErrorAction SilentlyContinue | Measure-Object).Count -eq 0) {
|
||||||
|
Remove-Item $downloadDir -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
291
Upload-Image.ps1
291
Upload-Image.ps1
@@ -1,11 +1,12 @@
|
|||||||
#
|
#
|
||||||
# Upload-Image.ps1 — Copy Media Creator Lite cached image to the PXE server
|
# Upload-Image.ps1 — Copy MCL cached image to the PXE server
|
||||||
#
|
#
|
||||||
# Copies Deploy/, Tools/, and Sources (from Boot/Sources.zip) to the
|
# Reads user_selections.json to upload only the selected OS, matching
|
||||||
# PXE server's image-upload share using robocopy with authentication.
|
# packages, and config files. Drivers are EXCLUDED by default.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# .\Upload-Image.ps1 (uses default MCL cache path)
|
# .\Upload-Image.ps1 (selected OS + packages, no drivers)
|
||||||
|
# .\Upload-Image.ps1 -IncludeDrivers (also upload selected hardware drivers)
|
||||||
# .\Upload-Image.ps1 -CachePath "D:\MCL\Cache" (custom cache location)
|
# .\Upload-Image.ps1 -CachePath "D:\MCL\Cache" (custom cache location)
|
||||||
# .\Upload-Image.ps1 -Server 10.9.100.1 (custom server IP)
|
# .\Upload-Image.ps1 -Server 10.9.100.1 (custom server IP)
|
||||||
#
|
#
|
||||||
@@ -18,11 +19,23 @@ param(
|
|||||||
[string]$Server = "10.9.100.1",
|
[string]$Server = "10.9.100.1",
|
||||||
[string]$User = "pxe-upload",
|
[string]$User = "pxe-upload",
|
||||||
[string]$Pass = "pxe",
|
[string]$Pass = "pxe",
|
||||||
[switch]$IncludeDell10
|
[switch]$IncludeDrivers
|
||||||
)
|
)
|
||||||
|
|
||||||
$Share = "\\$Server\image-upload"
|
$Share = "\\$Server\image-upload"
|
||||||
|
|
||||||
|
function Format-Size {
|
||||||
|
param([long]$Bytes)
|
||||||
|
if ($Bytes -ge 1GB) { return "{0:N1} GB" -f ($Bytes / 1GB) }
|
||||||
|
if ($Bytes -ge 1MB) { return "{0:N1} MB" -f ($Bytes / 1MB) }
|
||||||
|
return "{0:N0} KB" -f ($Bytes / 1KB)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-DestDir {
|
||||||
|
param([string]$Dir)
|
||||||
|
return ($Dir -replace '^\*destinationdir\*\\?', '')
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "========================================" -ForegroundColor Cyan
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
Write-Host " PXE Server Image Uploader" -ForegroundColor Cyan
|
Write-Host " PXE Server Image Uploader" -ForegroundColor Cyan
|
||||||
@@ -32,11 +45,6 @@ Write-Host ""
|
|||||||
# --- Validate source paths ---
|
# --- Validate source paths ---
|
||||||
$DeployPath = Join-Path $CachePath "Deploy"
|
$DeployPath = Join-Path $CachePath "Deploy"
|
||||||
$ToolsPath = Join-Path (Split-Path $CachePath -Parent) "Tools"
|
$ToolsPath = Join-Path (Split-Path $CachePath -Parent) "Tools"
|
||||||
# Tools is a sibling of Cache in the MCL directory structure
|
|
||||||
if (-not (Test-Path $ToolsPath -PathType Container)) {
|
|
||||||
# Fallback: try Tools inside CachePath parent's parent
|
|
||||||
$ToolsPath = Join-Path (Split-Path (Split-Path $CachePath -Parent) -Parent) "Tools"
|
|
||||||
}
|
|
||||||
if (-not (Test-Path $ToolsPath -PathType Container)) {
|
if (-not (Test-Path $ToolsPath -PathType Container)) {
|
||||||
$ToolsPath = "C:\ProgramData\GEAerospace\MediaCreator\Tools"
|
$ToolsPath = "C:\ProgramData\GEAerospace\MediaCreator\Tools"
|
||||||
}
|
}
|
||||||
@@ -44,24 +52,104 @@ $SourcesZip = Join-Path $CachePath "Boot\Sources.zip"
|
|||||||
|
|
||||||
if (-not (Test-Path $DeployPath -PathType Container)) {
|
if (-not (Test-Path $DeployPath -PathType Container)) {
|
||||||
Write-Host "ERROR: Deploy directory not found at $DeployPath" -ForegroundColor Red
|
Write-Host "ERROR: Deploy directory not found at $DeployPath" -ForegroundColor Red
|
||||||
Write-Host " Provide the correct cache path: .\Upload-Image.ps1 -CachePath ""D:\Path\To\Cache""" -ForegroundColor Yellow
|
Write-Host " .\Upload-Image.ps1 -CachePath ""D:\Path\To\Cache""" -ForegroundColor Yellow
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host " Cache Path: $CachePath"
|
# --- Parse user_selections.json ---
|
||||||
Write-Host " Deploy: $DeployPath" -ForegroundColor $(if (Test-Path $DeployPath) { "Green" } else { "Red" })
|
$SelectionsFile = Join-Path $ToolsPath "user_selections.json"
|
||||||
Write-Host " Tools: $ToolsPath" -ForegroundColor $(if (Test-Path $ToolsPath) { "Green" } else { "Yellow" })
|
if (-not (Test-Path $SelectionsFile)) {
|
||||||
Write-Host " Sources.zip: $SourcesZip" -ForegroundColor $(if (Test-Path $SourcesZip) { "Green" } else { "Yellow" })
|
Write-Host "ERROR: user_selections.json not found at $SelectionsFile" -ForegroundColor Red
|
||||||
Write-Host " Server: $Server"
|
Write-Host " Run Media Creator Lite first to create a configuration." -ForegroundColor Yellow
|
||||||
if (-not $IncludeDell10) {
|
exit 1
|
||||||
Write-Host " Excluding: Dell_10 drivers (use -IncludeDell10 to include)" -ForegroundColor Gray
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$selections = (Get-Content $SelectionsFile -Raw | ConvertFrom-Json)[0]
|
||||||
|
$selectedOsId = $selections.OperatingSystemSelection
|
||||||
|
$selectedModelIds = @($selections.HardwareModelSelection | ForEach-Object { $_.Id } | Select-Object -Unique)
|
||||||
|
|
||||||
|
# --- Parse control JSONs ---
|
||||||
|
$ControlPath = Join-Path $DeployPath "Control"
|
||||||
|
|
||||||
|
$osJsonFile = Join-Path $ControlPath "OperatingSystem.json"
|
||||||
|
$driverJsonFile = Join-Path $ControlPath "HardwareDriver.json"
|
||||||
|
$pkgJsonFile = Join-Path $ControlPath "packages.json"
|
||||||
|
|
||||||
|
if (-not (Test-Path $osJsonFile)) {
|
||||||
|
Write-Host "ERROR: OperatingSystem.json not found in $ControlPath" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$osJson = Get-Content $osJsonFile -Raw | ConvertFrom-Json
|
||||||
|
$driverJson = if (Test-Path $driverJsonFile) { Get-Content $driverJsonFile -Raw | ConvertFrom-Json } else { @() }
|
||||||
|
$pkgJson = if (Test-Path $pkgJsonFile) { Get-Content $pkgJsonFile -Raw | ConvertFrom-Json } else { @() }
|
||||||
|
|
||||||
|
# --- Resolve selections to paths ---
|
||||||
|
|
||||||
|
# OS: match OperatingSystemSelection ID to OperatingSystem.json entries
|
||||||
|
$matchedOs = @($osJson | Where-Object { $_.operatingSystemVersion.id -eq [int]$selectedOsId })
|
||||||
|
$osDirs = @()
|
||||||
|
$osTotalSize = [long]0
|
||||||
|
foreach ($os in $matchedOs) {
|
||||||
|
$rel = Resolve-DestDir $os.operatingSystemVersion.wim.DestinationDir
|
||||||
|
$osDirs += $rel
|
||||||
|
$osTotalSize += $os.operatingSystemVersion.wim.size
|
||||||
|
}
|
||||||
|
|
||||||
|
# Packages: enabled + matching OS ID
|
||||||
|
$matchedPkgs = @($pkgJson | Where-Object { $_.aOsIds -contains $selectedOsId -and $_.enabled -eq 1 })
|
||||||
|
$pkgTotalSize = [long]0
|
||||||
|
foreach ($pkg in $matchedPkgs) { $pkgTotalSize += $pkg.size }
|
||||||
|
|
||||||
|
# Drivers: match selected model IDs (family) + OS ID, deduplicate by path
|
||||||
|
$allMatchingDrivers = @($driverJson | Where-Object {
|
||||||
|
$selectedModelIds -contains $_.family -and $_.aOsIds -contains $selectedOsId
|
||||||
|
})
|
||||||
|
$allDriverDirSet = [ordered]@{}
|
||||||
|
foreach ($drv in $allMatchingDrivers) {
|
||||||
|
$rel = Resolve-DestDir $drv.DestinationDir
|
||||||
|
if (-not $allDriverDirSet.Contains($rel)) { $allDriverDirSet[$rel] = $drv.size }
|
||||||
|
}
|
||||||
|
$allDriverCount = $allDriverDirSet.Count
|
||||||
|
$allDriverTotalSize = [long]0
|
||||||
|
$allDriverDirSet.Values | ForEach-Object { $allDriverTotalSize += $_ }
|
||||||
|
|
||||||
|
$driverDirs = @()
|
||||||
|
$driverTotalSize = [long]0
|
||||||
|
if ($IncludeDrivers) {
|
||||||
|
$driverDirs = @($allDriverDirSet.Keys)
|
||||||
|
$driverTotalSize = $allDriverTotalSize
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Display upload plan ---
|
||||||
|
Write-Host " Cache: $CachePath"
|
||||||
|
Write-Host " Server: $Server"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Upload Plan (from user_selections.json):" -ForegroundColor Cyan
|
||||||
|
Write-Host " ------------------------------------------"
|
||||||
|
|
||||||
|
if ($matchedOs.Count -gt 0) {
|
||||||
|
$osName = $matchedOs[0].operatingSystemVersion.marketingName
|
||||||
|
Write-Host " OS: $osName ($(Format-Size $osTotalSize))" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " OS: No match for selection ID $selectedOsId" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host " Packages: $($matchedPkgs.Count) update(s) ($(Format-Size $pkgTotalSize))" -ForegroundColor Green
|
||||||
|
|
||||||
|
if ($IncludeDrivers) {
|
||||||
|
Write-Host " Drivers: $($driverDirs.Count) model(s) ($(Format-Size $driverTotalSize))" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " Drivers: SKIPPED -- $allDriverCount available, use -IncludeDrivers" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host " Control: Always included" -ForegroundColor Gray
|
||||||
|
Write-Host " Tools: $(if (Test-Path $ToolsPath) { 'Yes' } else { 'Not found' })" -ForegroundColor $(if (Test-Path $ToolsPath) { "Gray" } else { "Yellow" })
|
||||||
|
Write-Host " Sources: $(if (Test-Path $SourcesZip) { 'Yes (from Boot\Sources.zip)' } else { 'Not found' })" -ForegroundColor $(if (Test-Path $SourcesZip) { "Gray" } else { "Yellow" })
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|
||||||
# --- Connect to SMB share ---
|
# --- Connect to SMB share ---
|
||||||
Write-Host "Connecting to $Share ..." -ForegroundColor Gray
|
Write-Host "Connecting to $Share ..." -ForegroundColor Gray
|
||||||
|
|
||||||
# Remove any stale connection
|
|
||||||
net use $Share /delete 2>$null | Out-Null
|
net use $Share /delete 2>$null | Out-Null
|
||||||
|
|
||||||
$netResult = net use $Share /user:$User $Pass 2>&1
|
$netResult = net use $Share /user:$User $Pass 2>&1
|
||||||
@@ -79,47 +167,135 @@ Write-Host "Connected." -ForegroundColor Green
|
|||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|
||||||
$failed = $false
|
$failed = $false
|
||||||
|
$stepNum = 0
|
||||||
|
$totalSteps = 1 # Deploy base always
|
||||||
|
if ($matchedOs.Count -gt 0) { $totalSteps++ }
|
||||||
|
if ($matchedPkgs.Count -gt 0) { $totalSteps++ }
|
||||||
|
if ($IncludeDrivers -and $driverDirs.Count -gt 0) { $totalSteps++ }
|
||||||
|
if (Test-Path $ToolsPath -PathType Container) { $totalSteps++ }
|
||||||
|
if (Test-Path $SourcesZip) { $totalSteps++ }
|
||||||
|
|
||||||
# --- Step 1: Copy Deploy/ ---
|
# --- Step: Deploy base (Control, Applications, config -- skip big dirs) ---
|
||||||
|
$stepNum++
|
||||||
Write-Host "========================================" -ForegroundColor Cyan
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
Write-Host "[1/3] Copying Deploy/ ..." -ForegroundColor Cyan
|
Write-Host "[$stepNum/$totalSteps] Copying Deploy\ base (Control, Applications, config) ..." -ForegroundColor Cyan
|
||||||
Write-Host "========================================" -ForegroundColor Cyan
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
|
||||||
$robocopyArgs = @($DeployPath, "$Share\Deploy", "/E", "/R:3", "/W:5", "/NP", "/ETA")
|
robocopy $DeployPath "$Share\Deploy" /E /XD "Operating Systems" "Out-of-box Drivers" "Packages" /R:3 /W:5 /NP /ETA
|
||||||
if (-not $IncludeDell10) {
|
|
||||||
$robocopyArgs += @("/XD", "Dell_10")
|
|
||||||
}
|
|
||||||
& robocopy @robocopyArgs
|
|
||||||
if ($LASTEXITCODE -ge 8) {
|
if ($LASTEXITCODE -ge 8) {
|
||||||
Write-Host "ERROR: Deploy copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
|
Write-Host "ERROR: Deploy base copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
|
||||||
$failed = $true
|
$failed = $true
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Step 2: Copy Tools/ ---
|
# --- Step: Operating System ---
|
||||||
|
if ($matchedOs.Count -gt 0) {
|
||||||
|
$stepNum++
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "========================================" -ForegroundColor Cyan
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
Write-Host "[2/3] Copying Tools/ ..." -ForegroundColor Cyan
|
Write-Host "[$stepNum/$totalSteps] Copying Operating System ..." -ForegroundColor Cyan
|
||||||
Write-Host "========================================" -ForegroundColor Cyan
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
foreach ($osDir in $osDirs) {
|
||||||
|
$src = Join-Path $CachePath $osDir
|
||||||
|
$dst = Join-Path $Share $osDir
|
||||||
|
if (Test-Path $src -PathType Container) {
|
||||||
|
Write-Host " $osDir" -ForegroundColor Gray
|
||||||
|
robocopy $src $dst /E /R:3 /W:5 /NP /ETA
|
||||||
|
if ($LASTEXITCODE -ge 8) {
|
||||||
|
Write-Host "ERROR: OS copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
|
||||||
|
$failed = $true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " SKIPPED (not cached): $osDir" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Step: Packages ---
|
||||||
|
if ($matchedPkgs.Count -gt 0) {
|
||||||
|
$stepNum++
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host "[$stepNum/$totalSteps] Copying Packages ..." -ForegroundColor Cyan
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Group packages by destination directory for efficient robocopy
|
||||||
|
$pkgGroups = [ordered]@{}
|
||||||
|
foreach ($pkg in $matchedPkgs) {
|
||||||
|
$rel = Resolve-DestDir $pkg.destinationDir
|
||||||
|
if (-not $pkgGroups.Contains($rel)) { $pkgGroups[$rel] = @() }
|
||||||
|
$pkgGroups[$rel] += $pkg.fileName
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($dir in $pkgGroups.Keys) {
|
||||||
|
$src = Join-Path $CachePath $dir
|
||||||
|
$dst = Join-Path $Share $dir
|
||||||
|
$files = $pkgGroups[$dir]
|
||||||
|
if (Test-Path $src -PathType Container) {
|
||||||
|
foreach ($f in $files) { Write-Host " $f" -ForegroundColor Gray }
|
||||||
|
$robocopyArgs = @($src, $dst) + $files + @("/R:3", "/W:5", "/NP", "/ETA")
|
||||||
|
& robocopy @robocopyArgs
|
||||||
|
if ($LASTEXITCODE -ge 8) {
|
||||||
|
Write-Host "ERROR: Package copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
|
||||||
|
$failed = $true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " SKIPPED (not cached): $dir" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Step: Drivers (only with -IncludeDrivers) ---
|
||||||
|
if ($IncludeDrivers -and $driverDirs.Count -gt 0) {
|
||||||
|
$stepNum++
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host "[$stepNum/$totalSteps] Copying Drivers ($($driverDirs.Count) model(s)) ..." -ForegroundColor Cyan
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
$drvCopied = 0
|
||||||
|
foreach ($drvDir in $driverDirs) {
|
||||||
|
$drvCopied++
|
||||||
|
$src = Join-Path $CachePath $drvDir
|
||||||
|
$dst = Join-Path $Share $drvDir
|
||||||
|
if (Test-Path $src -PathType Container) {
|
||||||
|
Write-Host " [$drvCopied/$($driverDirs.Count)] $drvDir" -ForegroundColor Gray
|
||||||
|
robocopy $src $dst /E /R:3 /W:5 /NP /ETA
|
||||||
|
if ($LASTEXITCODE -ge 8) {
|
||||||
|
Write-Host "ERROR: Driver copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
|
||||||
|
$failed = $true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " SKIPPED (not cached): $drvDir" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Step: Tools ---
|
||||||
if (Test-Path $ToolsPath -PathType Container) {
|
if (Test-Path $ToolsPath -PathType Container) {
|
||||||
|
$stepNum++
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host "[$stepNum/$totalSteps] Copying Tools\ ..." -ForegroundColor Cyan
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
|
||||||
robocopy $ToolsPath "$Share\Tools" /E /R:3 /W:5 /NP /ETA
|
robocopy $ToolsPath "$Share\Tools" /E /R:3 /W:5 /NP /ETA
|
||||||
if ($LASTEXITCODE -ge 8) {
|
if ($LASTEXITCODE -ge 8) {
|
||||||
Write-Host "ERROR: Tools copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
|
Write-Host "ERROR: Tools copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
|
||||||
$failed = $true
|
$failed = $true
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Write-Host "SKIPPED: Tools directory not found at $ToolsPath" -ForegroundColor Yellow
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Step 3: Extract and copy Sources/ ---
|
# --- Step: Sources ---
|
||||||
|
$TempSources = $null
|
||||||
|
if (Test-Path $SourcesZip) {
|
||||||
|
$stepNum++
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "========================================" -ForegroundColor Cyan
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
Write-Host "[3/3] Extracting and copying Sources/ ..." -ForegroundColor Cyan
|
Write-Host "[$stepNum/$totalSteps] Extracting and copying Sources\ ..." -ForegroundColor Cyan
|
||||||
Write-Host "========================================" -ForegroundColor Cyan
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
|
||||||
if (Test-Path $SourcesZip) {
|
|
||||||
$TempExtract = Join-Path $env:TEMP "SourcesExtract"
|
$TempExtract = Join-Path $env:TEMP "SourcesExtract"
|
||||||
Write-Host " Extracting Sources.zip..."
|
|
||||||
Remove-Item -Recurse -Force $TempExtract -ErrorAction SilentlyContinue
|
Remove-Item -Recurse -Force $TempExtract -ErrorAction SilentlyContinue
|
||||||
Expand-Archive $SourcesZip -DestinationPath $TempExtract -Force
|
Expand-Archive $SourcesZip -DestinationPath $TempExtract -Force
|
||||||
|
|
||||||
@@ -135,10 +311,49 @@ if (Test-Path $SourcesZip) {
|
|||||||
$failed = $true
|
$failed = $true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Clean up temp extraction
|
|
||||||
Remove-Item -Recurse -Force $TempExtract -ErrorAction SilentlyContinue
|
Remove-Item -Recurse -Force $TempExtract -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Verify small files (SMB write-cache workaround) ---
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host "Verifying small files ..." -ForegroundColor Cyan
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
$fixCount = 0
|
||||||
|
$verifyPairs = @(
|
||||||
|
@{ Local = (Join-Path $DeployPath "Control"); Remote = "$Share\Deploy\Control" }
|
||||||
|
)
|
||||||
|
if (Test-Path $ToolsPath -PathType Container) {
|
||||||
|
$verifyPairs += @{ Local = $ToolsPath; Remote = "$Share\Tools" }
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($pair in $verifyPairs) {
|
||||||
|
$localDir = $pair.Local
|
||||||
|
$remoteDir = $pair.Remote
|
||||||
|
if (-not (Test-Path $localDir -PathType Container)) { continue }
|
||||||
|
|
||||||
|
Get-ChildItem -Path $localDir -Recurse -File -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.Length -gt 0 -and $_.Length -lt 1MB } |
|
||||||
|
ForEach-Object {
|
||||||
|
$rel = $_.FullName.Substring($localDir.Length)
|
||||||
|
$dstFile = Join-Path $remoteDir $rel
|
||||||
|
if (Test-Path $dstFile) {
|
||||||
|
$dstSize = (Get-Item $dstFile).Length
|
||||||
|
if ($dstSize -ne $_.Length) {
|
||||||
|
Write-Host " Fixing: $rel ($dstSize -> $($_.Length) bytes)" -ForegroundColor Yellow
|
||||||
|
$bytes = [System.IO.File]::ReadAllBytes($_.FullName)
|
||||||
|
[System.IO.File]::WriteAllBytes($dstFile, $bytes)
|
||||||
|
$fixCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fixCount -eq 0) {
|
||||||
|
Write-Host " All files verified OK." -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "SKIPPED: Sources.zip not found at $SourcesZip" -ForegroundColor Yellow
|
Write-Host " Fixed $fixCount file(s)." -ForegroundColor Yellow
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Disconnect ---
|
# --- Disconnect ---
|
||||||
|
|||||||
422
docs/Shopfloor_Display_MicroPC_Imaging_Guide.html
Normal file
422
docs/Shopfloor_Display_MicroPC_Imaging_Guide.html
Normal file
File diff suppressed because one or more lines are too long
40
docs/shopfloor-display-imaging-guide.md
Normal file
40
docs/shopfloor-display-imaging-guide.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Shopfloor Display MicroPC Imaging Guide
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
- MicroPC connected to PXE switch
|
||||||
|
- USB mouse and keyboard connected
|
||||||
|
- PXE server running and reachable
|
||||||
|
|
||||||
|
## Step 1: BIOS Configuration
|
||||||
|
|
||||||
|
1. Power on the MicroPC and **tap F12** to reach the One-Time-Boot menu.
|
||||||
|
2. Select **BIOS Setup**.
|
||||||
|
3. Enable **Advanced Setup**.
|
||||||
|
4. Select **Boot Configuration**:
|
||||||
|
- Verify **Enable Secure Boot** is **ON**
|
||||||
|
- Verify **Enable Microsoft UEFI CA** is set to **Enabled**
|
||||||
|
5. Select **Storage**:
|
||||||
|
- Verify **AHCI/NVMe** is selected
|
||||||
|
6. Select **Connection**:
|
||||||
|
- Verify **Integrated NIC** is set to **Enabled with PXE**
|
||||||
|
7. Click **Apply Changes**, then click **OK**.
|
||||||
|
8. Click **Exit** and immediately begin **tapping F12** again.
|
||||||
|
|
||||||
|
## Step 2: PXE Boot
|
||||||
|
|
||||||
|
1. Once you're back in the One-Time-Boot menu, select the **IPV4** option.
|
||||||
|
2. In the PXE Boot Menu, select **Windows PE (Image Deployment)** (auto-selected after 30 seconds).
|
||||||
|
3. Hit any key to verify Secure Boot is enabled, or wait 5 seconds to automatically continue.
|
||||||
|
|
||||||
|
## Step 3: Image Selection
|
||||||
|
|
||||||
|
1. For the WinPE Setup Menu, select **3. GEA Shopfloor**.
|
||||||
|
2. For GCCH Enrollment Profile, select **1. No Office**.
|
||||||
|
3. For Shopfloor PC Type, select **5. Display**.
|
||||||
|
4. For Display Type, select **2. Dashboard**.
|
||||||
|
|
||||||
|
## Step 4: Imaging
|
||||||
|
|
||||||
|
1. Once GE Image Setup launches, click **Start**.
|
||||||
|
2. From this point the process is mostly automated.
|
||||||
|
3. Note the **Serial Number** from the screen and let Patrick know it's a Shopfloor Display.
|
||||||
@@ -87,6 +87,16 @@
|
|||||||
<Path>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v BypassNRO /t REG_DWORD /d 1 /f</Path>
|
<Path>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v BypassNRO /t REG_DWORD /d 1 /f</Path>
|
||||||
<Description>Bypass OOBE network requirement</Description>
|
<Description>Bypass OOBE network requirement</Description>
|
||||||
</RunSynchronousCommand>
|
</RunSynchronousCommand>
|
||||||
|
<RunSynchronousCommand wcm:action="add">
|
||||||
|
<Order>14</Order>
|
||||||
|
<Path>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v SkipMachineOOBE /t REG_DWORD /d 1 /f</Path>
|
||||||
|
<Description>Skip machine OOBE phase</Description>
|
||||||
|
</RunSynchronousCommand>
|
||||||
|
<RunSynchronousCommand wcm:action="add">
|
||||||
|
<Order>15</Order>
|
||||||
|
<Path>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v SkipUserOOBE /t REG_DWORD /d 1 /f</Path>
|
||||||
|
<Description>Skip user OOBE phase</Description>
|
||||||
|
</RunSynchronousCommand>
|
||||||
</RunSynchronous>
|
</RunSynchronous>
|
||||||
</component>
|
</component>
|
||||||
</settings>
|
</settings>
|
||||||
@@ -151,11 +161,16 @@
|
|||||||
</SynchronousCommand>
|
</SynchronousCommand>
|
||||||
<SynchronousCommand wcm:action="add">
|
<SynchronousCommand wcm:action="add">
|
||||||
<Order>5</Order>
|
<Order>5</Order>
|
||||||
|
<CommandLine>powershell.exe -ExecutionPolicy Bypass -Command "Get-NetAdapter -Physical | Where-Object { $_.InterfaceDescription -notmatch 'Wi-Fi|Wireless' } | Disable-NetAdapter -Confirm:$false; while (-not (Test-Connection -ComputerName login.microsoftonline.us -Count 1 -Quiet -ErrorAction SilentlyContinue)) { Start-Sleep -Seconds 5 }; Write-Host 'Internet confirmed over WiFi.'"</CommandLine>
|
||||||
|
<Description>Disable wired adapters and wait for WiFi internet</Description>
|
||||||
|
</SynchronousCommand>
|
||||||
|
<SynchronousCommand wcm:action="add">
|
||||||
|
<Order>6</Order>
|
||||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\run-enrollment.ps1"</CommandLine>
|
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\run-enrollment.ps1"</CommandLine>
|
||||||
<Description>Run GCCH Enrollment</Description>
|
<Description>Run GCCH Enrollment</Description>
|
||||||
</SynchronousCommand>
|
</SynchronousCommand>
|
||||||
<SynchronousCommand wcm:action="add">
|
<SynchronousCommand wcm:action="add">
|
||||||
<Order>6</Order>
|
<Order>7</Order>
|
||||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Run-ShopfloorSetup.ps1"</CommandLine>
|
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Run-ShopfloorSetup.ps1"</CommandLine>
|
||||||
<Description>Run shopfloor PC type setup</Description>
|
<Description>Run shopfloor PC type setup</Description>
|
||||||
</SynchronousCommand>
|
</SynchronousCommand>
|
||||||
|
|||||||
@@ -37,11 +37,12 @@
|
|||||||
- gea-standard
|
- gea-standard
|
||||||
- gea-engineer
|
- gea-engineer
|
||||||
- gea-shopfloor
|
- gea-shopfloor
|
||||||
- gea-shopfloor-mce
|
|
||||||
- ge-standard
|
- ge-standard
|
||||||
- ge-engineer
|
- ge-engineer
|
||||||
- ge-shopfloor-lockdown
|
- ge-shopfloor-lockdown
|
||||||
- ge-shopfloor-mce
|
- ge-shopfloor-mce
|
||||||
|
shopfloor_types:
|
||||||
|
- gea-shopfloor
|
||||||
deploy_subdirs:
|
deploy_subdirs:
|
||||||
- Applications
|
- Applications
|
||||||
- Control
|
- Control
|
||||||
@@ -298,6 +299,36 @@
|
|||||||
state: directory
|
state: directory
|
||||||
mode: '0777'
|
mode: '0777'
|
||||||
|
|
||||||
|
- name: "Create enrollment packages directory"
|
||||||
|
file:
|
||||||
|
path: /srv/samba/enrollment
|
||||||
|
state: directory
|
||||||
|
mode: '0777'
|
||||||
|
|
||||||
|
- name: "Deploy shopfloor setup scripts to enrollment share"
|
||||||
|
copy:
|
||||||
|
src: "{{ usb_mount }}/shopfloor-setup/"
|
||||||
|
dest: /srv/samba/enrollment/shopfloor-setup/
|
||||||
|
mode: '0755'
|
||||||
|
directory_mode: '0755'
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- name: "Create BIOS update directory on enrollment share"
|
||||||
|
file:
|
||||||
|
path: /srv/samba/enrollment/BIOS
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: "Deploy BIOS check script and manifest"
|
||||||
|
copy:
|
||||||
|
src: "{{ usb_mount }}/shopfloor-setup/BIOS/{{ item }}"
|
||||||
|
dest: /srv/samba/enrollment/BIOS/{{ item }}
|
||||||
|
mode: '0644'
|
||||||
|
loop:
|
||||||
|
- check-bios.cmd
|
||||||
|
- models.txt
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
- name: "Create image upload staging directory"
|
- name: "Create image upload staging directory"
|
||||||
file:
|
file:
|
||||||
path: /home/pxe/image-upload
|
path: /home/pxe/image-upload
|
||||||
@@ -348,6 +379,15 @@
|
|||||||
force user = root
|
force user = root
|
||||||
comment = Blancco Drive Eraser reports
|
comment = Blancco Drive Eraser reports
|
||||||
|
|
||||||
|
[enrollment]
|
||||||
|
path = /srv/samba/enrollment
|
||||||
|
browseable = yes
|
||||||
|
read only = no
|
||||||
|
guest ok = no
|
||||||
|
valid users = pxe-upload
|
||||||
|
force user = root
|
||||||
|
comment = GCCH bulk enrollment packages
|
||||||
|
|
||||||
[image-upload]
|
[image-upload]
|
||||||
path = /home/pxe/image-upload
|
path = /home/pxe/image-upload
|
||||||
browseable = yes
|
browseable = yes
|
||||||
@@ -357,6 +397,9 @@
|
|||||||
force user = pxe
|
force user = pxe
|
||||||
force group = pxe
|
force group = pxe
|
||||||
comment = PXE image upload staging area
|
comment = PXE image upload staging area
|
||||||
|
oplocks = no
|
||||||
|
level2 oplocks = no
|
||||||
|
strict sync = yes
|
||||||
|
|
||||||
- name: "Create Samba users (pxe-upload and blancco)"
|
- name: "Create Samba users (pxe-upload and blancco)"
|
||||||
shell: |
|
shell: |
|
||||||
@@ -392,6 +435,15 @@
|
|||||||
force: no
|
force: no
|
||||||
loop: "{{ image_types }}"
|
loop: "{{ image_types }}"
|
||||||
|
|
||||||
|
- name: "Deploy shopfloor unattend.xml template"
|
||||||
|
copy:
|
||||||
|
src: "{{ usb_mount }}/FlatUnattendW10-shopfloor.xml"
|
||||||
|
dest: "{{ samba_share }}/{{ item }}/Deploy/FlatUnattendW10.xml"
|
||||||
|
mode: '0644'
|
||||||
|
force: no
|
||||||
|
loop: "{{ shopfloor_types }}"
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
- name: "Daily cron to create/refresh Media.tag for all images"
|
- name: "Daily cron to create/refresh Media.tag for all images"
|
||||||
copy:
|
copy:
|
||||||
content: |
|
content: |
|
||||||
@@ -635,6 +687,7 @@
|
|||||||
Environment=CLONEZILLA_SHARE=/srv/samba/clonezilla
|
Environment=CLONEZILLA_SHARE=/srv/samba/clonezilla
|
||||||
Environment=WEB_ROOT={{ web_root }}
|
Environment=WEB_ROOT={{ web_root }}
|
||||||
Environment=BLANCCO_REPORTS=/srv/samba/blancco-reports
|
Environment=BLANCCO_REPORTS=/srv/samba/blancco-reports
|
||||||
|
Environment=ENROLLMENT_SHARE=/srv/samba/enrollment
|
||||||
Environment=AUDIT_LOG=/var/log/pxe-webapp-audit.log
|
Environment=AUDIT_LOG=/var/log/pxe-webapp-audit.log
|
||||||
ExecStart=/usr/bin/python3 app.py
|
ExecStart=/usr/bin/python3 app.py
|
||||||
Restart=always
|
Restart=always
|
||||||
|
|||||||
158
playbook/shopfloor-setup/BIOS/check-bios.cmd
Normal file
158
playbook/shopfloor-setup/BIOS/check-bios.cmd
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
@echo off
|
||||||
|
REM check-bios.cmd - Check and apply Dell BIOS update from WinPE x64
|
||||||
|
REM Called from startnet.cmd before imaging menu
|
||||||
|
REM Requires: Flash64W.exe (Dell 64-Bit BIOS Flash Utility) in same directory
|
||||||
|
REM
|
||||||
|
REM Exit behavior:
|
||||||
|
REM - BIOS update applied -> reboots automatically (flashes during POST)
|
||||||
|
REM - Already up to date -> returns to startnet.cmd
|
||||||
|
REM - No match / no files -> returns to startnet.cmd
|
||||||
|
|
||||||
|
set BIOSDIR=%~dp0
|
||||||
|
set FLASH=%BIOSDIR%Flash64W.exe
|
||||||
|
set MANIFEST=%BIOSDIR%models.txt
|
||||||
|
|
||||||
|
if not exist "%FLASH%" (
|
||||||
|
echo Flash64W.exe not found, skipping BIOS check.
|
||||||
|
exit /b 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if not exist "%MANIFEST%" (
|
||||||
|
echo models.txt not found, skipping BIOS check.
|
||||||
|
exit /b 0
|
||||||
|
)
|
||||||
|
|
||||||
|
REM --- Get system model from WMI ---
|
||||||
|
set SYSMODEL=
|
||||||
|
for /f "skip=1 tokens=*" %%M in ('wmic csproduct get name 2^>NUL') do (
|
||||||
|
if not defined SYSMODEL set "SYSMODEL=%%M"
|
||||||
|
)
|
||||||
|
REM Trim trailing whitespace
|
||||||
|
for /f "tokens=*" %%a in ("%SYSMODEL%") do set "SYSMODEL=%%a"
|
||||||
|
|
||||||
|
if "%SYSMODEL%"=="" (
|
||||||
|
echo Could not detect system model, skipping BIOS check.
|
||||||
|
exit /b 0
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Model: %SYSMODEL%
|
||||||
|
|
||||||
|
REM --- Get current BIOS version ---
|
||||||
|
set BIOSVER=
|
||||||
|
for /f "skip=1 tokens=*" %%V in ('wmic bios get smbiosbiosversion 2^>NUL') do (
|
||||||
|
if not defined BIOSVER set "BIOSVER=%%V"
|
||||||
|
)
|
||||||
|
for /f "tokens=*" %%a in ("%BIOSVER%") do set "BIOSVER=%%a"
|
||||||
|
echo Current BIOS: %BIOSVER%
|
||||||
|
|
||||||
|
REM --- Read manifest and find matching BIOS file ---
|
||||||
|
REM Format: ModelSubstring|BIOSFile|Version
|
||||||
|
set BIOSFILE=
|
||||||
|
set TARGETVER=
|
||||||
|
for /f "usebackq eol=# tokens=1,2,3 delims=|" %%A in ("%MANIFEST%") do (
|
||||||
|
echo "%SYSMODEL%" | find /I "%%A" >NUL
|
||||||
|
if not errorlevel 1 (
|
||||||
|
set "BIOSFILE=%%B"
|
||||||
|
set "TARGETVER=%%C"
|
||||||
|
goto :found_bios
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
echo No BIOS update available for this model.
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
:found_bios
|
||||||
|
if not exist "%BIOSDIR%%BIOSFILE%" (
|
||||||
|
echo WARNING: %BIOSFILE% not found in BIOS folder.
|
||||||
|
exit /b 0
|
||||||
|
)
|
||||||
|
|
||||||
|
REM --- Skip if already at target version ---
|
||||||
|
echo.%BIOSVER%| find /I "%TARGETVER%" >NUL
|
||||||
|
if not errorlevel 1 goto :already_current
|
||||||
|
|
||||||
|
REM --- Compare versions to prevent downgrade ---
|
||||||
|
REM Split current and target into major.minor.patch and compare numerically
|
||||||
|
call :compare_versions "%BIOSVER%" "%TARGETVER%"
|
||||||
|
if "%VERCMP%"=="newer" goto :already_newer
|
||||||
|
goto :do_flash
|
||||||
|
|
||||||
|
:already_current
|
||||||
|
echo BIOS is already up to date - %TARGETVER%
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
:already_newer
|
||||||
|
echo Current BIOS %BIOSVER% is newer than target %TARGETVER% - skipping.
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
:do_flash
|
||||||
|
|
||||||
|
echo Update: %BIOSVER% -^> %TARGETVER%
|
||||||
|
echo Applying BIOS update (this may take a few minutes, do not power off)...
|
||||||
|
|
||||||
|
REM --- Run Flash64W.exe from BIOS directory to avoid UNC path issues ---
|
||||||
|
REM Exit codes: 0=success, 2=reboot needed, 3=already current, 6=reboot needed
|
||||||
|
pushd "%BIOSDIR%"
|
||||||
|
Flash64W.exe /b="%BIOSFILE%" /s /f /l=X:\bios-update.log
|
||||||
|
set FLASHRC=%ERRORLEVEL%
|
||||||
|
popd
|
||||||
|
echo Flash complete (exit code %FLASHRC%).
|
||||||
|
|
||||||
|
if "%FLASHRC%"=="3" (
|
||||||
|
echo BIOS is already up to date.
|
||||||
|
exit /b 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%FLASHRC%"=="0" (
|
||||||
|
echo BIOS update complete.
|
||||||
|
exit /b 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%FLASHRC%"=="2" goto :staged
|
||||||
|
if "%FLASHRC%"=="6" goto :staged
|
||||||
|
|
||||||
|
echo WARNING: Flash64W.exe returned unexpected code %FLASHRC%.
|
||||||
|
echo Check X:\bios-update.log for details.
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
:staged
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo BIOS update staged successfully.
|
||||||
|
echo It will flash during POST after the
|
||||||
|
echo post-imaging reboot.
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
REM ============================================================
|
||||||
|
REM compare_versions - Compare two dotted version strings
|
||||||
|
REM Usage: call :compare_versions "current" "target"
|
||||||
|
REM Sets VERCMP=newer if current > target, older if current < target, equal if same
|
||||||
|
REM ============================================================
|
||||||
|
:compare_versions
|
||||||
|
set "VERCMP=equal"
|
||||||
|
set "_CV=%~1"
|
||||||
|
set "_TV=%~2"
|
||||||
|
|
||||||
|
REM Parse current version parts
|
||||||
|
for /f "tokens=1,2,3 delims=." %%a in ("%_CV%") do (
|
||||||
|
set /a "C1=%%a" 2>NUL
|
||||||
|
set /a "C2=%%b" 2>NUL
|
||||||
|
set /a "C3=%%c" 2>NUL
|
||||||
|
)
|
||||||
|
REM Parse target version parts
|
||||||
|
for /f "tokens=1,2,3 delims=." %%a in ("%_TV%") do (
|
||||||
|
set /a "T1=%%a" 2>NUL
|
||||||
|
set /a "T2=%%b" 2>NUL
|
||||||
|
set /a "T3=%%c" 2>NUL
|
||||||
|
)
|
||||||
|
|
||||||
|
if %C1% GTR %T1% ( set "VERCMP=newer" & goto :eof )
|
||||||
|
if %C1% LSS %T1% ( set "VERCMP=older" & goto :eof )
|
||||||
|
if %C2% GTR %T2% ( set "VERCMP=newer" & goto :eof )
|
||||||
|
if %C2% LSS %T2% ( set "VERCMP=older" & goto :eof )
|
||||||
|
if %C3% GTR %T3% ( set "VERCMP=newer" & goto :eof )
|
||||||
|
if %C3% LSS %T3% ( set "VERCMP=older" & goto :eof )
|
||||||
|
set "VERCMP=equal"
|
||||||
|
goto :eof
|
||||||
45
playbook/shopfloor-setup/CMM/01-Setup-CMM.ps1
Normal file
45
playbook/shopfloor-setup/CMM/01-Setup-CMM.ps1
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# 01-Setup-CMM.ps1 — CMM-specific setup (runs after Shopfloor baseline)
|
||||||
|
# Installs Hexagon CLM Tools, PC-DMIS 2016, and PC-DMIS 2019 R2
|
||||||
|
|
||||||
|
Write-Host "=== CMM Setup ==="
|
||||||
|
|
||||||
|
$hexDir = "C:\Enrollment\shopfloor-setup\CMM\hexagon"
|
||||||
|
|
||||||
|
if (-not (Test-Path $hexDir)) {
|
||||||
|
Write-Warning "Hexagon folder not found at $hexDir — skipping CMM installs."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Find installers ---
|
||||||
|
$clm = Get-ChildItem -Path $hexDir -Filter "CLM_*.exe" | Select-Object -First 1
|
||||||
|
$pcdmis16 = Get-ChildItem -Path $hexDir -Filter "Pcdmis2016*x64.exe" | Select-Object -First 1
|
||||||
|
$pcdmis19 = Get-ChildItem -Path $hexDir -Filter "Pcdmis2019*x64.exe" | Select-Object -First 1
|
||||||
|
|
||||||
|
# --- 1. Install CLM Tools (license manager — must be first) ---
|
||||||
|
if ($clm) {
|
||||||
|
Write-Host "Installing CLM Tools: $($clm.Name)..."
|
||||||
|
$p = Start-Process -FilePath $clm.FullName -ArgumentList "-q -norestart" -Wait -PassThru
|
||||||
|
Write-Host " CLM Tools exit code: $($p.ExitCode)"
|
||||||
|
} else {
|
||||||
|
Write-Warning "CLM Tools installer not found in $hexDir (expected CLM_*.exe)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 2. Install PC-DMIS 2016 ---
|
||||||
|
if ($pcdmis16) {
|
||||||
|
Write-Host "Installing PC-DMIS 2016: $($pcdmis16.Name)..."
|
||||||
|
$p = Start-Process -FilePath $pcdmis16.FullName -ArgumentList "-q INSTALLPDFCONVERTER=0 INSTALLOFFLINEHELP=0 HEIP=0 -norestart" -Wait -PassThru
|
||||||
|
Write-Host " PC-DMIS 2016 exit code: $($p.ExitCode)"
|
||||||
|
} else {
|
||||||
|
Write-Warning "PC-DMIS 2016 installer not found in $hexDir (expected Pcdmis2016*x64.exe)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 3. Install PC-DMIS 2019 R2 ---
|
||||||
|
if ($pcdmis19) {
|
||||||
|
Write-Host "Installing PC-DMIS 2019 R2: $($pcdmis19.Name)..."
|
||||||
|
$p = Start-Process -FilePath $pcdmis19.FullName -ArgumentList "-q INSTALLPDFCONVERTER=0 INSTALLOFFLINEHELP=0 HEIP=0 -norestart" -Wait -PassThru
|
||||||
|
Write-Host " PC-DMIS 2019 exit code: $($p.ExitCode)"
|
||||||
|
} else {
|
||||||
|
Write-Warning "PC-DMIS 2019 installer not found in $hexDir (expected Pcdmis2019*x64.exe)"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "=== CMM Setup Complete ==="
|
||||||
13
playbook/shopfloor-setup/Check-Policies.bat
Normal file
13
playbook/shopfloor-setup/Check-Policies.bat
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
@echo off
|
||||||
|
title MDM Policy Check
|
||||||
|
powershell.exe -ExecutionPolicy Bypass -Command ^
|
||||||
|
"$path = 'HKLM:\SOFTWARE\Microsoft\PolicyManager\current\device\ADMX_Power';" ^
|
||||||
|
"Write-Host '';" ^
|
||||||
|
"if (Test-Path $path) {" ^
|
||||||
|
" Write-Host ' READY FOR LOCKDOWN ' -ForegroundColor White -BackgroundColor DarkGreen;" ^
|
||||||
|
"} else {" ^
|
||||||
|
" Write-Host ' NOT READY FOR LOCKDOWN ' -ForegroundColor White -BackgroundColor Red;" ^
|
||||||
|
"};" ^
|
||||||
|
"Write-Host '';" ^
|
||||||
|
"Write-Host 'Press any key to exit...';" ^
|
||||||
|
"$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')"
|
||||||
45
playbook/shopfloor-setup/Display/01-Setup-Display.ps1
Normal file
45
playbook/shopfloor-setup/Display/01-Setup-Display.ps1
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# 01-Setup-Display.ps1 — Display-specific setup (runs after Shopfloor baseline)
|
||||||
|
# Reads display-type.txt to install either LobbyDisplay or Dashboard kiosk app.
|
||||||
|
|
||||||
|
$enrollDir = "C:\Enrollment"
|
||||||
|
$typeFile = Join-Path $enrollDir "display-type.txt"
|
||||||
|
$setupDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
|
||||||
|
if (-not (Test-Path $typeFile)) {
|
||||||
|
Write-Warning "No display-type.txt found - skipping display setup."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$displayType = (Get-Content $typeFile -First 1).Trim()
|
||||||
|
Write-Host "=== Display Setup: $displayType ==="
|
||||||
|
|
||||||
|
switch ($displayType) {
|
||||||
|
"Lobby" {
|
||||||
|
$installer = Join-Path $setupDir "GEAerospaceLobbyDisplaySetup.exe"
|
||||||
|
$appName = "Lobby Display"
|
||||||
|
}
|
||||||
|
"Dashboard" {
|
||||||
|
$installer = Join-Path $setupDir "GEAerospaceDashboardSetup.exe"
|
||||||
|
$appName = "Dashboard"
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
Write-Warning "Unknown display type: $displayType"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $installer)) {
|
||||||
|
Write-Warning "$appName installer not found at $installer - skipping."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Installing $appName..."
|
||||||
|
$proc = Start-Process -FilePath $installer -ArgumentList '/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', "/LOG=C:\Enrollment\$appName-install.log" -Wait -PassThru
|
||||||
|
|
||||||
|
if ($proc.ExitCode -eq 0) {
|
||||||
|
Write-Host "$appName installed successfully."
|
||||||
|
} else {
|
||||||
|
Write-Warning "$appName exited with code $($proc.ExitCode). Check C:\Enrollment\$appName-install.log"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "=== Display Setup Complete ==="
|
||||||
14
playbook/shopfloor-setup/Genspect/01-Setup-Genspect.ps1
Normal file
14
playbook/shopfloor-setup/Genspect/01-Setup-Genspect.ps1
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# 01-Setup-Genspect.ps1 — Genspect-specific setup (runs after Shopfloor baseline)
|
||||||
|
|
||||||
|
Write-Host "=== Genspect Setup ==="
|
||||||
|
|
||||||
|
# --- Add Genspect credentials ---
|
||||||
|
# cmdkey /generic:genspect-server /user:domain\genspectuser /pass:password
|
||||||
|
|
||||||
|
# --- Install Genspect applications ---
|
||||||
|
# Start-Process msiexec.exe -ArgumentList '/i "C:\Enrollment\shopfloor-setup\Genspect\GenspectApp.msi" /qn' -Wait
|
||||||
|
|
||||||
|
# --- Genspect configuration ---
|
||||||
|
# Set-ItemProperty -Path "HKLM:\SOFTWARE\CompanyName" -Name "PCType" -Value "Genspect"
|
||||||
|
|
||||||
|
Write-Host "=== Genspect Setup Complete ==="
|
||||||
14
playbook/shopfloor-setup/Keyence/01-Setup-Keyence.ps1
Normal file
14
playbook/shopfloor-setup/Keyence/01-Setup-Keyence.ps1
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# 01-Setup-Keyence.ps1 — Keyence-specific setup (runs after Shopfloor baseline)
|
||||||
|
|
||||||
|
Write-Host "=== Keyence Setup ==="
|
||||||
|
|
||||||
|
# --- Add Keyence credentials ---
|
||||||
|
# cmdkey /generic:keyence-server /user:domain\keyenceuser /pass:password
|
||||||
|
|
||||||
|
# --- Install Keyence applications ---
|
||||||
|
# Start-Process msiexec.exe -ArgumentList '/i "C:\Enrollment\shopfloor-setup\Keyence\KeyenceApp.msi" /qn' -Wait
|
||||||
|
|
||||||
|
# --- Keyence configuration ---
|
||||||
|
# Set-ItemProperty -Path "HKLM:\SOFTWARE\CompanyName" -Name "PCType" -Value "Keyence"
|
||||||
|
|
||||||
|
Write-Host "=== Keyence Setup Complete ==="
|
||||||
@@ -4,6 +4,19 @@
|
|||||||
# Cancel any pending reboot so it doesn't interrupt setup
|
# Cancel any pending reboot so it doesn't interrupt setup
|
||||||
shutdown -a 2>$null
|
shutdown -a 2>$null
|
||||||
|
|
||||||
|
# Prompt user to unplug from PXE switch before re-enabling wired adapters
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "========================================" -ForegroundColor Yellow
|
||||||
|
Write-Host " UNPLUG the ethernet cable from the" -ForegroundColor Yellow
|
||||||
|
Write-Host " PXE imaging switch NOW." -ForegroundColor Yellow
|
||||||
|
Write-Host "========================================" -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Press any key to continue..." -ForegroundColor Yellow
|
||||||
|
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
||||||
|
|
||||||
|
# Re-enable wired adapters
|
||||||
|
Get-NetAdapter -Physical | Where-Object { $_.InterfaceDescription -notmatch 'Wi-Fi|Wireless' } | Enable-NetAdapter -Confirm:$false -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
$enrollDir = "C:\Enrollment"
|
$enrollDir = "C:\Enrollment"
|
||||||
$typeFile = Join-Path $enrollDir "pc-type.txt"
|
$typeFile = Join-Path $enrollDir "pc-type.txt"
|
||||||
$setupDir = Join-Path $enrollDir "shopfloor-setup"
|
$setupDir = Join-Path $enrollDir "shopfloor-setup"
|
||||||
@@ -56,5 +69,17 @@ if ($pcType -ne "Shopfloor") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Shopfloor setup complete for $pcType."
|
Write-Host "Shopfloor setup complete for $pcType."
|
||||||
|
|
||||||
|
# Copy backup lockdown script to SupportUser desktop
|
||||||
|
$lockdownScript = Join-Path $setupDir "backup_lockdown.bat"
|
||||||
|
if (Test-Path $lockdownScript) {
|
||||||
|
Copy-Item -Path $lockdownScript -Destination "C:\Users\SupportUser\Desktop\backup_lockdown.bat" -Force
|
||||||
|
Write-Host "backup_lockdown.bat copied to desktop."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set auto-logon to expire after 2 more logins
|
||||||
|
reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoLogonCount /t REG_DWORD /d 2 /f | Out-Null
|
||||||
|
Write-Host "Auto-logon set to 2 remaining logins."
|
||||||
|
|
||||||
Write-Host "Rebooting in 10 seconds..."
|
Write-Host "Rebooting in 10 seconds..."
|
||||||
shutdown /r /t 10
|
shutdown /r /t 10
|
||||||
|
|||||||
51
playbook/shopfloor-setup/Shopfloor/02-OpenTextCSF.ps1
Normal file
51
playbook/shopfloor-setup/Shopfloor/02-OpenTextCSF.ps1
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# 02-OpenTextCSF.ps1 — Deploy OpenText HostExplorer CSF profiles (baseline)
|
||||||
|
# Copies connection profiles, keymaps, menus, and macros to ProgramData.
|
||||||
|
|
||||||
|
$setupDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$csfSource = Join-Path $setupDir "csf"
|
||||||
|
$destRoot = "C:\ProgramData\Hummingbird\Connectivity\15.00\Shared"
|
||||||
|
|
||||||
|
if (-not (Test-Path $csfSource)) {
|
||||||
|
Write-Warning "CSF source folder not found at $csfSource - skipping."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Deploying OpenText CSF profiles to $destRoot ..."
|
||||||
|
|
||||||
|
# Map of source subdirectories to destination subdirectories
|
||||||
|
$folders = @(
|
||||||
|
@{ Src = "Profile"; Dest = "Profile" }
|
||||||
|
@{ Src = "Accessories\EB"; Dest = "Accessories\EB" }
|
||||||
|
@{ Src = "HostExplorer\Keymap"; Dest = "HostExplorer\Keymap" }
|
||||||
|
@{ Src = "HostExplorer\Menu"; Dest = "HostExplorer\Menu" }
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($folder in $folders) {
|
||||||
|
$src = Join-Path $csfSource $folder.Src
|
||||||
|
$dest = Join-Path $destRoot $folder.Dest
|
||||||
|
|
||||||
|
if (-not (Test-Path $src)) {
|
||||||
|
Write-Host " Skipping $($folder.Src) (not present in csf source)"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $dest)) {
|
||||||
|
New-Item -Path $dest -ItemType Directory -Force | Out-Null
|
||||||
|
Write-Host " Created $dest"
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = Get-ChildItem -Path $src -File
|
||||||
|
foreach ($file in $files) {
|
||||||
|
Copy-Item -Path $file.FullName -Destination $dest -Force
|
||||||
|
Write-Host " Copied $($file.Name) -> $dest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy pre-made .lnk shortcuts to Public Desktop
|
||||||
|
$lnkFiles = Get-ChildItem -Path $csfSource -Filter "*.lnk" -File
|
||||||
|
foreach ($lnk in $lnkFiles) {
|
||||||
|
Copy-Item -Path $lnk.FullName -Destination "C:\Users\Public\Desktop" -Force
|
||||||
|
Write-Host " Copied $($lnk.Name) -> Public Desktop"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "OpenText CSF deployment complete."
|
||||||
46
playbook/shopfloor-setup/Shopfloor/03-StartMenu.ps1
Normal file
46
playbook/shopfloor-setup/Shopfloor/03-StartMenu.ps1
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# 03-StartMenu.ps1 — Create Start Menu shortcuts for all users (baseline)
|
||||||
|
# Shortcuts in ProgramData\Microsoft\Windows\Start Menu\Programs\ persist for all accounts.
|
||||||
|
|
||||||
|
$startMenu = "C:\ProgramData\Microsoft\Windows\Start Menu\Programs"
|
||||||
|
$shell = New-Object -ComObject WScript.Shell
|
||||||
|
|
||||||
|
# --- Defect Tracker ---
|
||||||
|
$lnk = $shell.CreateShortcut("$startMenu\Defect Tracker.lnk")
|
||||||
|
$lnk.TargetPath = "S:\DT\Defect_Tracker\Defect_Tracker.application"
|
||||||
|
$lnk.Save()
|
||||||
|
Write-Host "Created Start Menu shortcut: Defect Tracker"
|
||||||
|
|
||||||
|
# --- Plant Applications (Edge) ---
|
||||||
|
$lnk = $shell.CreateShortcut("$startMenu\Plant Applications.lnk")
|
||||||
|
$lnk.TargetPath = "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
|
||||||
|
$lnk.Arguments = "https://mes-wjefferson.apps.lr.geaerospace.net/run/?app_name=Plant%20Applications"
|
||||||
|
$lnk.Save()
|
||||||
|
Write-Host "Created Start Menu shortcut: Plant Applications"
|
||||||
|
|
||||||
|
# --- ShopDB ---
|
||||||
|
$lnk = $shell.CreateShortcut("$startMenu\ShopDB.lnk")
|
||||||
|
$lnk.TargetPath = "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
|
||||||
|
$lnk.Arguments = "http://tsgwp00524.logon.ds.ge.com"
|
||||||
|
$lnk.Save()
|
||||||
|
Write-Host "Created Start Menu shortcut: ShopDB"
|
||||||
|
|
||||||
|
# --- Shopfloor Dashboard ---
|
||||||
|
$lnk = $shell.CreateShortcut("$startMenu\Shopfloor Dashboard.lnk")
|
||||||
|
$lnk.TargetPath = "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
|
||||||
|
$lnk.Arguments = "https://tsgwp00525.wjs.geaerospace.net/shopdb/shopfloor-dashboard/"
|
||||||
|
$lnk.Save()
|
||||||
|
Write-Host "Created Start Menu shortcut: Shopfloor Dashboard"
|
||||||
|
|
||||||
|
# --- ShopDB (GEA) ---
|
||||||
|
$lnk = $shell.CreateShortcut("$startMenu\ShopDB (GEA).lnk")
|
||||||
|
$lnk.TargetPath = "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
|
||||||
|
$lnk.Arguments = "https://tsgwp00525.wjs.geaerospace.net/shopdb/"
|
||||||
|
$lnk.Save()
|
||||||
|
Write-Host "Created Start Menu shortcut: ShopDB (GEA)"
|
||||||
|
|
||||||
|
# --- Add more shortcuts below ---
|
||||||
|
# $lnk = $shell.CreateShortcut("$startMenu\AppName.lnk")
|
||||||
|
# $lnk.TargetPath = "C:\Path\To\App.exe"
|
||||||
|
# $lnk.Save()
|
||||||
|
|
||||||
|
Write-Host "Start Menu shortcuts complete."
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# 04-NetworkAndWinRM.ps1 — Set network profiles to Private and enable WinRM (baseline)
|
||||||
|
|
||||||
|
# --- Set all network profiles to Private ---
|
||||||
|
Get-NetConnectionProfile | Set-NetConnectionProfile -NetworkCategory Private
|
||||||
|
Write-Host "All network profiles set to Private."
|
||||||
|
|
||||||
|
# --- Enable and configure WinRM ---
|
||||||
|
Enable-PSRemoting -Force -SkipNetworkProfileCheck
|
||||||
|
Write-Host "WinRM enabled."
|
||||||
24
playbook/shopfloor-setup/Shopfloor/05-PowerAndDisplay.ps1
Normal file
24
playbook/shopfloor-setup/Shopfloor/05-PowerAndDisplay.ps1
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# 05-PowerAndDisplay.ps1 — Shopfloor power plan and display settings (baseline)
|
||||||
|
|
||||||
|
# --- Set display timeout to Never (0 = never) on AC and DC ---
|
||||||
|
powercfg /change monitor-timeout-ac 0
|
||||||
|
powercfg /change monitor-timeout-dc 0
|
||||||
|
powercfg /change standby-timeout-ac 0
|
||||||
|
powercfg /change standby-timeout-dc 0
|
||||||
|
Write-Host "Power: display and standby set to Never."
|
||||||
|
|
||||||
|
# --- Set High Performance power plan ---
|
||||||
|
$highPerf = powercfg /list | Select-String "High performance"
|
||||||
|
if ($highPerf -match "([a-f0-9-]{36})") {
|
||||||
|
powercfg /setactive $Matches[1]
|
||||||
|
Write-Host "Power plan set to High Performance."
|
||||||
|
} else {
|
||||||
|
Write-Host "High Performance plan not found, using current plan."
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Disable lock screen timeout (screen saver) ---
|
||||||
|
Set-ItemProperty -Path "HKCU:\Control Panel\Desktop" -Name ScreenSaveActive -Value "0" -Force
|
||||||
|
Set-ItemProperty -Path "HKCU:\Control Panel\Desktop" -Name ScreenSaveTimeOut -Value "0" -Force
|
||||||
|
Set-ItemProperty -Path "HKCU:\Control Panel\Desktop" -Name "SCRNSAVE.EXE" -Value "" -Force
|
||||||
|
Write-Host "Screen saver set to None and disabled."
|
||||||
|
|
||||||
51
playbook/shopfloor-setup/Shopfloor/csf/Accessories/EB/Office.ebs
Executable file
51
playbook/shopfloor-setup/Shopfloor/csf/Accessories/EB/Office.ebs
Executable file
@@ -0,0 +1,51 @@
|
|||||||
|
'----------------------------------------------------------------------
|
||||||
|
' This macro was created by the macro recorder.
|
||||||
|
' Macro File: Office.ebs
|
||||||
|
' Date: Wed May 18 09:55:44 2016
|
||||||
|
' Recorded for profile: WJ_Office
|
||||||
|
'----------------------------------------------------------------------
|
||||||
|
|
||||||
|
Sub Main
|
||||||
|
Dim HostExplorer as Object
|
||||||
|
Dim MyHost as Object
|
||||||
|
Dim Rc as Integer
|
||||||
|
|
||||||
|
Dim iPSUpdateTimeout
|
||||||
|
Dim iWaitForStringTimeout
|
||||||
|
|
||||||
|
On Error goto GenericErrorHandler
|
||||||
|
|
||||||
|
Set HostExplorer = CreateObject("HostExplorer") ' Initialize HostExplorer Object
|
||||||
|
Set MyHost = HostExplorer.HostFromProfile("WJ_Office") ' Set object for the desired session
|
||||||
|
If MyHost is Nothing Then Goto NoSession
|
||||||
|
|
||||||
|
iPSUpdateTimeout = 60 ' WaitPSUpdated timeout set to 60 seconds
|
||||||
|
iWaitForStringTimeout = 60 ' WaitForString timeout set to 60 seconds
|
||||||
|
|
||||||
|
Rc = MyHost.WaitForString( "Username:", -1, iWaitForStringTimeout, TRUE )
|
||||||
|
If Rc = 0 Then Goto OnWaitForStringTimeout
|
||||||
|
Rc = MyHost.Keys("shop_pc^M")
|
||||||
|
Rc = MyHost.WaitPSUpdated( iPSUpdateTimeout, TRUE )
|
||||||
|
If Rc <> 0 Then Goto OnWaitPSUpdatedTimeout
|
||||||
|
Rc = MyHost.WaitForString( "Do you have a barcode reader?", -1, iWaitForStringTimeout, TRUE )
|
||||||
|
If Rc = 0 Then Goto OnWaitForStringTimeout
|
||||||
|
Exit Sub
|
||||||
|
|
||||||
|
'-------------------- Runtime Error Handlers --------------------
|
||||||
|
GenericErrorHandler:
|
||||||
|
Msgbox "Error " & Err & " : """ & Error(Err) & """ has occurred on line " & Erl-1 & "." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
|
||||||
|
Exit Sub
|
||||||
|
|
||||||
|
NoSession:
|
||||||
|
Msgbox "Profile ""WJ_Office"" is not running." & Chr(10) & "Unable to execute macro.", 16, "HostExplorer Macro Error"
|
||||||
|
Exit Sub
|
||||||
|
|
||||||
|
OnWaitPSUpdatedTimeout:
|
||||||
|
Msgbox "Timeout occured waiting for host to update screen." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
|
||||||
|
Exit Sub
|
||||||
|
|
||||||
|
OnWaitForStringTimeout:
|
||||||
|
Msgbox "Timeout occured waiting for string on host screen." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
|
||||||
|
Exit Sub
|
||||||
|
|
||||||
|
End Sub
|
||||||
35
playbook/shopfloor-setup/Shopfloor/csf/Accessories/EB/mmcs.ebs
Executable file
35
playbook/shopfloor-setup/Shopfloor/csf/Accessories/EB/mmcs.ebs
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
'----------------------------------------------------------------------
|
||||||
|
' This macro was created by the macro recorder.
|
||||||
|
' Macro File: mmcs.ebs
|
||||||
|
' Date: Tue Jun 04 08:56:51 2013
|
||||||
|
' Recorded for profile: mmcs
|
||||||
|
'----------------------------------------------------------------------
|
||||||
|
|
||||||
|
Sub Main
|
||||||
|
Dim HostExplorer as Object
|
||||||
|
Dim MyHost as Object
|
||||||
|
Dim iIdleTime
|
||||||
|
|
||||||
|
Dim iPSUpdateTime
|
||||||
|
|
||||||
|
On Error goto ErrorCheck
|
||||||
|
|
||||||
|
Set HostExplorer = CreateObject("HostExplorer") ' Initialize HostExplorer Object
|
||||||
|
Set MyHost = HostExplorer.HostFromProfile("mmcs") ' Set object for the desired session
|
||||||
|
|
||||||
|
iPSUpdateTime = 60 ' PS Update wait time set to 60 seconds
|
||||||
|
iIdleTime = 2000 ' Idle time set to 2000 milliseconds
|
||||||
|
|
||||||
|
MyHost.WaitPSUpdated(iPSUpdateTime)
|
||||||
|
Rc = MyHost.WaitForString("Username:", -1, 9999, TRUE)
|
||||||
|
MyHost.Keys("mmcswj^M")
|
||||||
|
MyHost.WaitPSUpdated(iPSUpdateTime)
|
||||||
|
Rc = MyHost.WaitForString("| Badge : |", -1, 9999, TRUE)
|
||||||
|
Exit Sub
|
||||||
|
|
||||||
|
ErrorCheck:
|
||||||
|
if (Err = 440) Then
|
||||||
|
Msgbox "The specified session is not running.", 16, "Hummingbird Macro Error"
|
||||||
|
End If
|
||||||
|
Exit Sub
|
||||||
|
End Sub
|
||||||
56
playbook/shopfloor-setup/Shopfloor/csf/Accessories/EB/shopfloor.ebs
Executable file
56
playbook/shopfloor-setup/Shopfloor/csf/Accessories/EB/shopfloor.ebs
Executable file
@@ -0,0 +1,56 @@
|
|||||||
|
'----------------------------------------------------------------------
|
||||||
|
' This macro was created by the macro recorder.
|
||||||
|
' Macro File: shopfloor.ebs
|
||||||
|
' Date: Wed May 18 09:57:00 2016
|
||||||
|
' Recorded for profile: WJ Shopfloor
|
||||||
|
'----------------------------------------------------------------------
|
||||||
|
|
||||||
|
Sub Main
|
||||||
|
Dim HostExplorer as Object
|
||||||
|
Dim MyHost as Object
|
||||||
|
Dim Rc as Integer
|
||||||
|
|
||||||
|
Dim iPSUpdateTimeout
|
||||||
|
Dim iWaitForStringTimeout
|
||||||
|
|
||||||
|
On Error goto GenericErrorHandler
|
||||||
|
|
||||||
|
Set HostExplorer = CreateObject("HostExplorer") ' Initialize HostExplorer Object
|
||||||
|
Set MyHost = HostExplorer.HostFromProfile("WJ Shopfloor") ' Set object for the desired session
|
||||||
|
If MyHost is Nothing Then Goto NoSession
|
||||||
|
|
||||||
|
iPSUpdateTimeout = 60 ' WaitPSUpdated timeout set to 60 seconds
|
||||||
|
iWaitForStringTimeout = 60 ' WaitForString timeout set to 60 seconds
|
||||||
|
|
||||||
|
Rc = MyHost.WaitForString( "Username:", -1, iWaitForStringTimeout, TRUE )
|
||||||
|
If Rc = 0 Then Goto OnWaitForStringTimeout
|
||||||
|
Rc = MyHost.Keys("shop_xmi^M")
|
||||||
|
Rc = MyHost.WaitPSUpdated( iPSUpdateTimeout, TRUE )
|
||||||
|
If Rc <> 0 Then Goto OnWaitPSUpdatedTimeout
|
||||||
|
Rc = MyHost.WaitForString( "Password:", -1, iWaitForStringTimeout, TRUE )
|
||||||
|
If Rc = 0 Then Goto OnWaitForStringTimeout
|
||||||
|
Rc = MyHost.Keys("dnc123^M")
|
||||||
|
Rc = MyHost.WaitPSUpdated( iPSUpdateTimeout, TRUE )
|
||||||
|
If Rc <> 0 Then Goto OnWaitPSUpdatedTimeout
|
||||||
|
Rc = MyHost.WaitForString( "| EXIT | |", -1, iWaitForStringTimeout, TRUE )
|
||||||
|
If Rc = 0 Then Goto OnWaitForStringTimeout
|
||||||
|
Exit Sub
|
||||||
|
|
||||||
|
'-------------------- Runtime Error Handlers --------------------
|
||||||
|
GenericErrorHandler:
|
||||||
|
Msgbox "Error " & Err & " : """ & Error(Err) & """ has occurred on line " & Erl-1 & "." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
|
||||||
|
Exit Sub
|
||||||
|
|
||||||
|
NoSession:
|
||||||
|
Msgbox "Profile ""WJ Shopfloor"" is not running." & Chr(10) & "Unable to execute macro.", 16, "HostExplorer Macro Error"
|
||||||
|
Exit Sub
|
||||||
|
|
||||||
|
OnWaitPSUpdatedTimeout:
|
||||||
|
Msgbox "Timeout occured waiting for host to update screen." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
|
||||||
|
Exit Sub
|
||||||
|
|
||||||
|
OnWaitForStringTimeout:
|
||||||
|
Msgbox "Timeout occured waiting for string on host screen." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
|
||||||
|
Exit Sub
|
||||||
|
|
||||||
|
End Sub
|
||||||
9
playbook/shopfloor-setup/Shopfloor/csf/HostExplorer/Keymap/Default.kmv
Executable file
9
playbook/shopfloor-setup/Shopfloor/csf/HostExplorer/Keymap/Default.kmv
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
[KEYMAP]
|
||||||
|
Normal Entries=1
|
||||||
|
Normal0=Backspace,Delete
|
||||||
|
Shift Entries=0
|
||||||
|
Ctrl Entries=0
|
||||||
|
ShiftCtrl Entries=0
|
||||||
|
Alt Entries=0
|
||||||
|
ShiftAlt Entries=0
|
||||||
|
AltCtrl Entries=0
|
||||||
13
playbook/shopfloor-setup/Shopfloor/csf/HostExplorer/Keymap/Office.kmv
Executable file
13
playbook/shopfloor-setup/Shopfloor/csf/HostExplorer/Keymap/Office.kmv
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
[KEYMAP]
|
||||||
|
Normal Entries=5
|
||||||
|
Normal0=F4,Pf4
|
||||||
|
Normal1=Subtract,Num-Pad-Minus
|
||||||
|
Normal2=Multiply,Num-Pad-*
|
||||||
|
Normal3=Divide,Num-Pad-/
|
||||||
|
Normal4=Backspace,Delete
|
||||||
|
Shift Entries=0
|
||||||
|
Ctrl Entries=0
|
||||||
|
ShiftCtrl Entries=0
|
||||||
|
Alt Entries=0
|
||||||
|
ShiftAlt Entries=0
|
||||||
|
AltCtrl Entries=0
|
||||||
23
playbook/shopfloor-setup/Shopfloor/csf/HostExplorer/Menu/Default Shop.hmv
Executable file
23
playbook/shopfloor-setup/Shopfloor/csf/HostExplorer/Menu/Default Shop.hmv
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
[Version]
|
||||||
|
AssemblyVersion=1
|
||||||
|
[Console]
|
||||||
|
Title=HostExplorer Menu Editor
|
||||||
|
IconPath=HumCSSPlugins.HumCSSActiveTunnelsPlugin
|
||||||
|
IconID=IDI_CSS_CONSOLE
|
||||||
|
[Attributes]
|
||||||
|
SupportAssemblyEditing=1
|
||||||
|
SupportSeparator=1
|
||||||
|
ParentLevels=100
|
||||||
|
[Parent Node]
|
||||||
|
name=Menu
|
||||||
|
[SubParent Node]
|
||||||
|
name=Submenu
|
||||||
|
[Child Node]
|
||||||
|
name=Menu Option
|
||||||
|
[Messages]
|
||||||
|
Create New Parent=Create New Menu
|
||||||
|
Create New SubParent=Create New Submenu
|
||||||
|
Insert New Parent=Insert New Menu
|
||||||
|
Insert New SubParent=Insert New Submenu
|
||||||
|
[Root Group]
|
||||||
|
Name=Menu
|
||||||
474
playbook/shopfloor-setup/Shopfloor/csf/HostExplorer/Menu/Default VT.hmv
Executable file
474
playbook/shopfloor-setup/Shopfloor/csf/HostExplorer/Menu/Default VT.hmv
Executable file
@@ -0,0 +1,474 @@
|
|||||||
|
[version]
|
||||||
|
AssemblyVersion=1
|
||||||
|
|
||||||
|
[nls]
|
||||||
|
filename=menunls$LANGUAGE_EXTENSION$.hma
|
||||||
|
|
||||||
|
[Console]
|
||||||
|
Title=HostExplorer Menu Editor
|
||||||
|
IconPath=HumCSSPlugins.HumCSSActiveTunnelsPlugin
|
||||||
|
IconID=IDI_CSS_CONSOLE
|
||||||
|
|
||||||
|
[Attributes]
|
||||||
|
SupportAssemblyEditing=1
|
||||||
|
SupportEnablingDisablingItems=0
|
||||||
|
SupportSeparator=1
|
||||||
|
|
||||||
|
[Parent Node]
|
||||||
|
Nameid=Parent
|
||||||
|
|
||||||
|
[SubParent Node]
|
||||||
|
Nameid=SubParent
|
||||||
|
|
||||||
|
[Child Node]
|
||||||
|
Nameid=Child
|
||||||
|
|
||||||
|
[Messages]
|
||||||
|
Nameid1=Create New Parent
|
||||||
|
Nameid2=Create New SubParent
|
||||||
|
Nameid3=Insert New Parent
|
||||||
|
Nameid4=Insert New SubParent
|
||||||
|
|
||||||
|
[root group]
|
||||||
|
Name=Menu
|
||||||
|
item1=File
|
||||||
|
item2=Edit
|
||||||
|
item3=Transfer
|
||||||
|
item4=Fonts
|
||||||
|
item5=options
|
||||||
|
item6=tools
|
||||||
|
item7=view
|
||||||
|
item8=window
|
||||||
|
item9=help
|
||||||
|
|
||||||
|
[Separator]
|
||||||
|
name=
|
||||||
|
id=0
|
||||||
|
|
||||||
|
[File]
|
||||||
|
Nameid=File
|
||||||
|
id=4071
|
||||||
|
Parent=1
|
||||||
|
item1=new session
|
||||||
|
item2=duplicate session
|
||||||
|
item3=open session
|
||||||
|
item4=open session in same window
|
||||||
|
item5=save session profile
|
||||||
|
item6=close session
|
||||||
|
item7=separator
|
||||||
|
item8=open layout
|
||||||
|
item9=save layout
|
||||||
|
item10=separator
|
||||||
|
item11=connect
|
||||||
|
item12=disconnect
|
||||||
|
item13=separator
|
||||||
|
item14=print screen
|
||||||
|
item15=print multiple screens
|
||||||
|
item16=save screen
|
||||||
|
item17=send screen
|
||||||
|
item18=separator
|
||||||
|
item19=screen capture
|
||||||
|
item20=separator
|
||||||
|
item21=Recent Sessions Submenu
|
||||||
|
item22=separator
|
||||||
|
item23=exit all
|
||||||
|
|
||||||
|
[New Session]
|
||||||
|
nameid=New Session
|
||||||
|
id=Dlg-New-Session
|
||||||
|
|
||||||
|
[Duplicate Session]
|
||||||
|
nameid=Duplicate Session
|
||||||
|
id=Duplicate-Session
|
||||||
|
|
||||||
|
[Open Session]
|
||||||
|
nameid=Open Session
|
||||||
|
id=Dlg-Open-Session
|
||||||
|
|
||||||
|
[Open Session In Same Window]
|
||||||
|
nameid=Open Session In Same Window
|
||||||
|
id=Dlg-Open-Session-In-Same-Window
|
||||||
|
|
||||||
|
[Save Session Profile]
|
||||||
|
nameid=Save Session Profile
|
||||||
|
id=Dlg-Save-Profile
|
||||||
|
|
||||||
|
[Close Session]
|
||||||
|
nameid=Close Session
|
||||||
|
id=Dlg-Close-Session
|
||||||
|
|
||||||
|
[open layout]
|
||||||
|
nameid=Open Layout
|
||||||
|
id=Dlg-Open-Layout
|
||||||
|
|
||||||
|
[save layout]
|
||||||
|
nameid=Save Layout
|
||||||
|
id=Dlg-Save-Layout
|
||||||
|
|
||||||
|
[connect]
|
||||||
|
nameid=connect
|
||||||
|
id=Connect
|
||||||
|
|
||||||
|
[disconnect]
|
||||||
|
nameid=disconnect
|
||||||
|
id=Disconnect
|
||||||
|
|
||||||
|
[print screen]
|
||||||
|
nameid=print screen
|
||||||
|
id=Dlg-Print-Screen
|
||||||
|
|
||||||
|
[print multiple screens]
|
||||||
|
nameid=print multiple screens
|
||||||
|
id=Dlg-Print-Multiple-Screens
|
||||||
|
|
||||||
|
|
||||||
|
[save screen]
|
||||||
|
nameid=Save Screen
|
||||||
|
id=Dlg-Save-Screen
|
||||||
|
|
||||||
|
[send screen]
|
||||||
|
nameid=Send Screen
|
||||||
|
id=Send-Screen
|
||||||
|
|
||||||
|
[screen capture]
|
||||||
|
nameid=Screen Capture
|
||||||
|
id=Toggle-Capture
|
||||||
|
|
||||||
|
[Recent Sessions Submenu]
|
||||||
|
nameid=Recent Sessions
|
||||||
|
id=4187
|
||||||
|
Parent=1
|
||||||
|
item1=recent sessions
|
||||||
|
|
||||||
|
[recent sessions]
|
||||||
|
nameid=Recent Sessions
|
||||||
|
id=Recent-Sessions
|
||||||
|
|
||||||
|
[Exit all]
|
||||||
|
nameid=exit all
|
||||||
|
id=Dlg-Exit
|
||||||
|
|
||||||
|
|
||||||
|
[Edit]
|
||||||
|
Nameid=Edit
|
||||||
|
id=4072
|
||||||
|
Parent=1
|
||||||
|
item1=copy
|
||||||
|
item2=copy append
|
||||||
|
item3=paste
|
||||||
|
item4=separator
|
||||||
|
item5=select all
|
||||||
|
item6=separator
|
||||||
|
item7=edit find
|
||||||
|
item8=separator
|
||||||
|
item9=clear display
|
||||||
|
item10=clear all
|
||||||
|
item11=soft terminal reset
|
||||||
|
|
||||||
|
[undo]
|
||||||
|
nameid=undo
|
||||||
|
id=Edit-Undo
|
||||||
|
|
||||||
|
[redo]
|
||||||
|
nameid=redo
|
||||||
|
id=Edit-Redo
|
||||||
|
|
||||||
|
[cut]
|
||||||
|
nameid=cut
|
||||||
|
id=Edit-Cut
|
||||||
|
|
||||||
|
[Copy]
|
||||||
|
nameid=copy
|
||||||
|
id=Edit-Copy
|
||||||
|
|
||||||
|
[Copy append]
|
||||||
|
nameid=Copy Append
|
||||||
|
id=Edit-Copy-Append
|
||||||
|
|
||||||
|
[Paste]
|
||||||
|
nameid=Paste
|
||||||
|
id=Edit-Paste
|
||||||
|
|
||||||
|
[Paste continue]
|
||||||
|
nameid=Paste Continue
|
||||||
|
id=Edit-Paste-Continue
|
||||||
|
|
||||||
|
[Select All]
|
||||||
|
nameid=Select All
|
||||||
|
id=Edit-SelectAll
|
||||||
|
|
||||||
|
[Edit Options]
|
||||||
|
nameid=Options
|
||||||
|
id=Dlg-Options
|
||||||
|
|
||||||
|
[edit find]
|
||||||
|
nameid=Find
|
||||||
|
id=Dlg-Find
|
||||||
|
|
||||||
|
[clear display]
|
||||||
|
nameid=Clear Display
|
||||||
|
id=Clear-Display
|
||||||
|
|
||||||
|
[clear all]
|
||||||
|
nameid=Clear All
|
||||||
|
id=Clear-Buffer
|
||||||
|
|
||||||
|
[soft terminal reset]
|
||||||
|
nameid=Soft Terminal Reset
|
||||||
|
id=Power-On-Reset
|
||||||
|
|
||||||
|
|
||||||
|
[Transfer]
|
||||||
|
Nameid=Transfer
|
||||||
|
id=4073
|
||||||
|
Parent=1
|
||||||
|
item1=send
|
||||||
|
item2=receive
|
||||||
|
|
||||||
|
[send]
|
||||||
|
nameid=Send File to Host
|
||||||
|
id=Dlg-Upload
|
||||||
|
|
||||||
|
[receive]
|
||||||
|
nameid=Receive File from Host
|
||||||
|
id=Dlg-Download
|
||||||
|
|
||||||
|
|
||||||
|
[Fonts]
|
||||||
|
Nameid=Fonts
|
||||||
|
id=4074
|
||||||
|
Parent=1
|
||||||
|
item1=next larger
|
||||||
|
item2=next smaller
|
||||||
|
item3=choose font
|
||||||
|
item4=maximize font
|
||||||
|
|
||||||
|
[next larger]
|
||||||
|
nameid=next larger font
|
||||||
|
id=Font-Larger
|
||||||
|
|
||||||
|
[next smaller]
|
||||||
|
nameid=Next smaller font
|
||||||
|
id=Font-Smaller
|
||||||
|
|
||||||
|
[choose font]
|
||||||
|
nameid=font
|
||||||
|
id=Dlg-Font-Select
|
||||||
|
|
||||||
|
[maximize font]
|
||||||
|
nameid=Maximize font
|
||||||
|
id=Maximize-Font
|
||||||
|
|
||||||
|
|
||||||
|
[options]
|
||||||
|
nameid=Options
|
||||||
|
id=4075
|
||||||
|
Parent=1
|
||||||
|
item1=global options
|
||||||
|
item2=api settings
|
||||||
|
item3=separator
|
||||||
|
item4=keyboard mapping
|
||||||
|
item5=quick keys
|
||||||
|
item6=separator
|
||||||
|
item7=session properties
|
||||||
|
|
||||||
|
[global options]
|
||||||
|
nameid=Global Options
|
||||||
|
id=Dlg-Global
|
||||||
|
|
||||||
|
[api settings]
|
||||||
|
nameid=API Settings
|
||||||
|
id=Dlg-API-Settings
|
||||||
|
|
||||||
|
[keyboard mapping]
|
||||||
|
nameid=Keyboard Mapping
|
||||||
|
id=Dlg-Keyboard-Mapper
|
||||||
|
|
||||||
|
[quick keys]
|
||||||
|
nameid=Quick Keys
|
||||||
|
id=Dlg-Quick-Key-Editor
|
||||||
|
|
||||||
|
[session properties]
|
||||||
|
nameid=session Properties
|
||||||
|
id=Dlg-Edit-Session-Profile
|
||||||
|
|
||||||
|
|
||||||
|
[tools]
|
||||||
|
nameid=Tools
|
||||||
|
id=4076
|
||||||
|
Parent=1
|
||||||
|
item1=Macro Submenu
|
||||||
|
item2=Quick Script Submenu
|
||||||
|
item3=separator
|
||||||
|
item4=Customizetoolbar
|
||||||
|
item5=Customizemenu
|
||||||
|
item6=Customizesessionproperties
|
||||||
|
|
||||||
|
[Macro Submenu]
|
||||||
|
nameid=Macro
|
||||||
|
id=4177
|
||||||
|
Parent=1
|
||||||
|
item1=edit macro
|
||||||
|
item2=run macro
|
||||||
|
item3=separator
|
||||||
|
item4=start recording
|
||||||
|
item5=pause recording
|
||||||
|
item6=resume recording
|
||||||
|
item7=stop recording
|
||||||
|
item8=cancel recording
|
||||||
|
|
||||||
|
[Edit macro]
|
||||||
|
nameid=Macro Edit
|
||||||
|
id=Dlg-Edit-Macro
|
||||||
|
|
||||||
|
[run macro]
|
||||||
|
nameid=run
|
||||||
|
id=Dlg-Run-Macro
|
||||||
|
|
||||||
|
[start recording]
|
||||||
|
nameid=Start Recording
|
||||||
|
id=Record-Macro
|
||||||
|
|
||||||
|
[pause recording]
|
||||||
|
nameid=Pause Recording
|
||||||
|
id=Toggle-Recording-Pause
|
||||||
|
|
||||||
|
[resume recording]
|
||||||
|
nameid=Resume Recording
|
||||||
|
id=Resume-Recording-Macro
|
||||||
|
|
||||||
|
[stop recording]
|
||||||
|
nameid=Stop Recording
|
||||||
|
id=End-Recording
|
||||||
|
|
||||||
|
[cancel recording]
|
||||||
|
nameid=Cancel Recording
|
||||||
|
id=Cancel-Macro-Recording
|
||||||
|
|
||||||
|
[Quick Script Submenu]
|
||||||
|
nameid=Quick Script
|
||||||
|
id=4091
|
||||||
|
Parent=1
|
||||||
|
item1=edit qs
|
||||||
|
item2=run qs
|
||||||
|
item3=stop qs
|
||||||
|
item4=separator
|
||||||
|
item5=start recording qs
|
||||||
|
item6=pause recording qs
|
||||||
|
item7=resume recording qs
|
||||||
|
item8=stop recording qs
|
||||||
|
item9=cancel recording qs
|
||||||
|
|
||||||
|
[Customizetoolbar]
|
||||||
|
nameid=Customize Toolbar
|
||||||
|
id=Dlg-Toolbars
|
||||||
|
|
||||||
|
[Customizemenu]
|
||||||
|
nameid=Customize Menu
|
||||||
|
id=Dlg-Customize-Menus
|
||||||
|
|
||||||
|
[CustomizeSessionProperties]
|
||||||
|
nameid=Customize Session Properties
|
||||||
|
id=Dlg-Customize-Session-Properties
|
||||||
|
|
||||||
|
[Edit qs]
|
||||||
|
nameid=Edit
|
||||||
|
id=Dlg-QuickScript-Editor
|
||||||
|
|
||||||
|
[run qs]
|
||||||
|
nameid=Run
|
||||||
|
id=Dlg-Run-QuickScript
|
||||||
|
|
||||||
|
[stop qs]
|
||||||
|
nameid=Stop
|
||||||
|
id=Stop-QuickScript
|
||||||
|
|
||||||
|
[start recording qs]
|
||||||
|
nameid=Start Recording
|
||||||
|
id=Record-QuickScript
|
||||||
|
|
||||||
|
[pause recording qs]
|
||||||
|
nameid=Pause Recording
|
||||||
|
id=Pause-Recording-QuickScript
|
||||||
|
|
||||||
|
[resume recording qs]
|
||||||
|
nameid=Resume Recording
|
||||||
|
id=Resume-Recording-QuickScript
|
||||||
|
|
||||||
|
[stop recording qs]
|
||||||
|
nameid=Stop Recording
|
||||||
|
id=Stop-Recording-QuickScript
|
||||||
|
|
||||||
|
[cancel recording qs]
|
||||||
|
nameid=Cancel Recording
|
||||||
|
id=Cancel-Recording-QuickScript
|
||||||
|
|
||||||
|
|
||||||
|
[view]
|
||||||
|
nameid=view
|
||||||
|
id=4178
|
||||||
|
Parent=1
|
||||||
|
item1=hotspots
|
||||||
|
item2=row and column indicator
|
||||||
|
item3=cross hair cursor
|
||||||
|
item4=full screen
|
||||||
|
|
||||||
|
[hotspots]
|
||||||
|
nameid=Hotspots
|
||||||
|
id=Toggle-View-Hotspots
|
||||||
|
|
||||||
|
[Row and Column Indicator]
|
||||||
|
nameid=Row and Column Indicator
|
||||||
|
id=Toggle-Row-And-Column-Indicator
|
||||||
|
|
||||||
|
[cross hair cursor]
|
||||||
|
nameid=Cross-Hair Cursor
|
||||||
|
id=Toggle-CrossHair-Cursor
|
||||||
|
|
||||||
|
[full screen]
|
||||||
|
nameid=Full Screen
|
||||||
|
id=Toggle-Full-Screen
|
||||||
|
|
||||||
|
|
||||||
|
[window]
|
||||||
|
nameid=Window
|
||||||
|
id=4179
|
||||||
|
Parent=1
|
||||||
|
item1=cascade
|
||||||
|
item2=next session
|
||||||
|
item3=previous session
|
||||||
|
item4=separator
|
||||||
|
item5=session window list
|
||||||
|
|
||||||
|
[cascade]
|
||||||
|
nameid=Cascade
|
||||||
|
id=Cascade-Session-Windows
|
||||||
|
|
||||||
|
[next session]
|
||||||
|
nameid=next session
|
||||||
|
id=Next-Session
|
||||||
|
|
||||||
|
[previous session]
|
||||||
|
nameid=previous session
|
||||||
|
id=Prev-Session
|
||||||
|
|
||||||
|
[session window list]
|
||||||
|
name=<List Of Windows>
|
||||||
|
id=4142
|
||||||
|
|
||||||
|
|
||||||
|
[help]
|
||||||
|
nameid=Help
|
||||||
|
id=4180
|
||||||
|
Parent=1
|
||||||
|
item1=contents
|
||||||
|
item2=separator
|
||||||
|
item3=about
|
||||||
|
|
||||||
|
[contents]
|
||||||
|
nameid=Contents
|
||||||
|
id=Help-Index
|
||||||
|
|
||||||
|
[about]
|
||||||
|
nameid=about
|
||||||
|
id=Help-About
|
||||||
BIN
playbook/shopfloor-setup/Shopfloor/csf/IBM_qks.lnk
Executable file
BIN
playbook/shopfloor-setup/Shopfloor/csf/IBM_qks.lnk
Executable file
Binary file not shown.
106
playbook/shopfloor-setup/Shopfloor/csf/Profile/IBM_qks.hep
Executable file
106
playbook/shopfloor-setup/Shopfloor/csf/Profile/IBM_qks.hep
Executable file
@@ -0,0 +1,106 @@
|
|||||||
|
[PROFILE]
|
||||||
|
Session Properties=Default 3270
|
||||||
|
Menu=Default 3270
|
||||||
|
Auto Run Macro Delay Time=1
|
||||||
|
Gateway Name=ibmqks.ae.ge.com
|
||||||
|
Session Keep Alive Interval=600
|
||||||
|
PC Keyboard Type=5
|
||||||
|
IND$FILE Parms=0,0,1,1,0,0,0,257,253,2048,200,430,430,0,1252,"IND$FILE","","","",0
|
||||||
|
SSL/TLS Current User Certificate=1
|
||||||
|
Window Title=%s - %p (%h)
|
||||||
|
WinInfo Default Restored=0 0 0 0 70 405 822 595 0 0 0=20 10 0 0 400 0 0 0 0 3 2 2 0 HE_Bitmap| 0 0
|
||||||
|
WinInfo Default Maximized=0 0 0 0 -8 -8 1936 1048 0 0 0=26 12 0 0 400 0 0 0 0 3 2 2 0 HE_Bitmap| 0 0
|
||||||
|
Print Header2=Page: %# Document Name: %F
|
||||||
|
QuickKey=Default
|
||||||
|
Never Run Profile=Off
|
||||||
|
Profile File Version=200
|
||||||
|
Space Allocation Parms=0,0,0,0
|
||||||
|
Kerberos Use Hummingbird Kerberos=On
|
||||||
|
Cryptographic Mode=1
|
||||||
|
IP Port=992
|
||||||
|
Security Option=2
|
||||||
|
SSL/TLS User Cipher Suites=DEFAULT
|
||||||
|
[HEBar-59440]
|
||||||
|
sizeFloatCX=505
|
||||||
|
sizeFloatCY=28
|
||||||
|
[Toolbars Layout-Summary]
|
||||||
|
Bars=3
|
||||||
|
ScreenCX=1920
|
||||||
|
ScreenCY=1080
|
||||||
|
[Session Options]
|
||||||
|
Last Selected Page=139
|
||||||
|
[Toolbars]
|
||||||
|
ToolBarVersion=6
|
||||||
|
bar0=Default 3270 ToolBar.tb3
|
||||||
|
[Toolbars Layout-Bar0]
|
||||||
|
BarID=59393
|
||||||
|
[Toolbars Layout-Bar1]
|
||||||
|
BarID=59419
|
||||||
|
Bars=3
|
||||||
|
Bar#0=0
|
||||||
|
Bar#1=59440
|
||||||
|
Bar#2=0
|
||||||
|
[Toolbars Layout-Bar2]
|
||||||
|
BarID=59440
|
||||||
|
XPos=-2
|
||||||
|
YPos=-2
|
||||||
|
Docking=1
|
||||||
|
MRUDockID=59419
|
||||||
|
MRUDockLeftPos=-2
|
||||||
|
MRUDockTopPos=-2
|
||||||
|
MRUDockRightPos=512
|
||||||
|
MRUDockBottomPos=26
|
||||||
|
MRUFloatStyle=8196
|
||||||
|
MRUFloatXPos=-1
|
||||||
|
MRUFloatYPos=1025
|
||||||
|
[Hotspots]
|
||||||
|
ShowTips=1
|
||||||
|
[ShortcutReplacers]
|
||||||
|
Size=0
|
||||||
|
[Site Info]
|
||||||
|
EnableCache=Yes
|
||||||
|
RefreshCacheOnConnect=0
|
||||||
|
AlwaysUploadUsing=0
|
||||||
|
AlwaysDownloadUsing=0
|
||||||
|
ProfileVersion=5
|
||||||
|
SecurityType=0
|
||||||
|
Port=21
|
||||||
|
SSL Implicit Port=990
|
||||||
|
Directory2=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
SFTPProxyAddress=localhost
|
||||||
|
SFTPProxyPort=0
|
||||||
|
SFTPWriteSize=256
|
||||||
|
GssApiServiceName=host
|
||||||
|
GssApiProtLevel=P
|
||||||
|
GssApiIssueCCC=No
|
||||||
|
GssApiPBSZ=16000
|
||||||
|
GssApiKerbClient=0
|
||||||
|
GssApiUseHMS2MIT=No
|
||||||
|
PassiveMode=2
|
||||||
|
AllowServer2ServerTransfer=0
|
||||||
|
KeepAlive=No
|
||||||
|
AutoDetectServerType=Yes
|
||||||
|
KeepAliveTime=0
|
||||||
|
FirewallPort=21
|
||||||
|
FirewallType=0
|
||||||
|
NetworkTimeout=120
|
||||||
|
ConvertGMTTime=0
|
||||||
|
ResolveLinks=1
|
||||||
|
RootToInitialDir=0
|
||||||
|
ChangeDirChangesRoot=0
|
||||||
|
DropVmsVersion=0
|
||||||
|
LocalHourOffset=0
|
||||||
|
LocalMinuteOffset=0
|
||||||
|
TemporaryProfile=0
|
||||||
|
SSLVersion=3
|
||||||
|
SSLReuse=0
|
||||||
|
SSLUserCertificateMode=0
|
||||||
|
SSLCloseOnNegotiateionFail=0
|
||||||
|
SSLAcceptServerSelfSignedCertificates=Yes
|
||||||
|
SSLAcceptServerUnverifiedCertificates=Yes
|
||||||
|
SSLDataChannelProtected=Yes
|
||||||
|
SSLCryptographicMode=1
|
||||||
|
AllowResume=No
|
||||||
|
EPSV4=0
|
||||||
|
[Stats]
|
||||||
|
NumVisits=0
|
||||||
122
playbook/shopfloor-setup/Shopfloor/csf/Profile/WJ Shopfloor.hep
Executable file
122
playbook/shopfloor-setup/Shopfloor/csf/Profile/WJ Shopfloor.hep
Executable file
@@ -0,0 +1,122 @@
|
|||||||
|
[PROFILE]
|
||||||
|
Session Terminal Type=2
|
||||||
|
Session Properties=Default VT
|
||||||
|
Menu=Default VT
|
||||||
|
Auto Run Macro Delay Time=1
|
||||||
|
Gateway Name=wjfms3.apps.wlm.geaerospace.net
|
||||||
|
Session Keep Alive Interval=600
|
||||||
|
PC Keyboard Type=5
|
||||||
|
Tabstops='8 16 24 32 40 48 56 64 72 '
|
||||||
|
VT BG Colors='1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 8 7 7 9 15 14 10 2 10 '
|
||||||
|
Default VT Recv Path=$MYDOCUMENTS$\
|
||||||
|
SSL/TLS Version=3
|
||||||
|
SSL/TLS Current User Certificate=1
|
||||||
|
Window Title=%s - %p (%h)
|
||||||
|
WinInfo Default Restored=0 0 0 0 176 21 892 619 0 0 0=-17 0 0 0 400 0 0 0 0 3 2 2 0 Courier New| 0 0
|
||||||
|
WinInfo Default Maximized=0 0 0 0 -8 -8 1634 1066 0 0 0=30 15 0 0 700 0 0 0 1 3 2 2 0 HE_Bitmap| 0 0
|
||||||
|
Print Header2=Page: %# Document Name: %F
|
||||||
|
QuickKey=Super Key F6
|
||||||
|
Never Run Profile=Off
|
||||||
|
dwVTFlags1=29369344
|
||||||
|
VT FG Colors='7 7 9 15 14 10 2 10 0 1 2 3 4 5 15 7 8 9 10 11 12 13 14 15 15 0 0 1 0 0 0 0 0 0 0 '
|
||||||
|
Connect Timeout=60
|
||||||
|
Disable Secure Connection Warning=Yes
|
||||||
|
Profile File Version=200
|
||||||
|
Cryptographic Mode=1
|
||||||
|
Display RowCol Indicator=Off
|
||||||
|
VT Scrollback Save Attribs=Off
|
||||||
|
VT True Display Mode=1
|
||||||
|
Status Line=1
|
||||||
|
Kerberos Use Hummingbird Kerberos=On
|
||||||
|
Keyboard=Office
|
||||||
|
Auto Start Quick-Key=shopfloor.ebs
|
||||||
|
On Disconnect=0
|
||||||
|
Allow sleep while connected=0
|
||||||
|
Toolbar Scheme Name=Default VT
|
||||||
|
Save Profile on Window Close=Off
|
||||||
|
Toolbar Scheme Modified=Default VT:30026745--1815298816
|
||||||
|
Save Toolbar Info on Exit=Off
|
||||||
|
Save Mode=0
|
||||||
|
[Session Options]
|
||||||
|
Last Selected Page=4000
|
||||||
|
[HEBar-59440]
|
||||||
|
[Toolbars Layout-Summary]
|
||||||
|
Bars=3
|
||||||
|
ScreenCX=1600
|
||||||
|
ScreenCY=1200
|
||||||
|
[Toolbars]
|
||||||
|
ToolBarVersion=5
|
||||||
|
bar0=Default VT ToolBar.tbv
|
||||||
|
[Toolbars Layout-Bar0]
|
||||||
|
BarID=59393
|
||||||
|
Visible=0
|
||||||
|
[Toolbars Layout-Bar1]
|
||||||
|
BarID=59419
|
||||||
|
Bars=3
|
||||||
|
Bar#0=0
|
||||||
|
Bar#1=59440
|
||||||
|
Bar#2=0
|
||||||
|
[Toolbars Layout-Bar2]
|
||||||
|
BarID=59440
|
||||||
|
XPos=-2
|
||||||
|
YPos=-2
|
||||||
|
Docking=1
|
||||||
|
MRUDockID=59419
|
||||||
|
MRUDockLeftPos=-2
|
||||||
|
MRUDockTopPos=-2
|
||||||
|
MRUDockRightPos=541
|
||||||
|
MRUDockBottomPos=26
|
||||||
|
MRUFloatStyle=8196
|
||||||
|
MRUFloatXPos=-2147483648
|
||||||
|
MRUFloatYPos=0
|
||||||
|
[Hotspots]
|
||||||
|
ShowTips=1
|
||||||
|
[ShortcutReplacers]
|
||||||
|
Size=0
|
||||||
|
[Site Info]
|
||||||
|
EnableCache=Yes
|
||||||
|
RefreshCacheOnConnect=0
|
||||||
|
AlwaysUploadUsing=0
|
||||||
|
AlwaysDownloadUsing=0
|
||||||
|
ProfileVersion=5
|
||||||
|
SecurityType=0
|
||||||
|
Port=21
|
||||||
|
SSL Implicit Port=990
|
||||||
|
Directory2=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
SFTPProxyAddress=localhost
|
||||||
|
SFTPProxyPort=0
|
||||||
|
SFTPWriteSize=256
|
||||||
|
GssApiServiceName=host
|
||||||
|
GssApiProtLevel=P
|
||||||
|
GssApiIssueCCC=No
|
||||||
|
GssApiPBSZ=16000
|
||||||
|
GssApiKerbClient=0
|
||||||
|
GssApiUseHMS2MIT=No
|
||||||
|
PassiveMode=2
|
||||||
|
AllowServer2ServerTransfer=0
|
||||||
|
KeepAlive=No
|
||||||
|
AutoDetectServerType=Yes
|
||||||
|
KeepAliveTime=0
|
||||||
|
FirewallPort=21
|
||||||
|
FirewallType=0
|
||||||
|
NetworkTimeout=120
|
||||||
|
ConvertGMTTime=0
|
||||||
|
ResolveLinks=1
|
||||||
|
RootToInitialDir=0
|
||||||
|
ChangeDirChangesRoot=0
|
||||||
|
DropVmsVersion=0
|
||||||
|
LocalHourOffset=0
|
||||||
|
LocalMinuteOffset=0
|
||||||
|
TemporaryProfile=0
|
||||||
|
SSLVersion=3
|
||||||
|
SSLReuse=0
|
||||||
|
SSLUserCertificateMode=0
|
||||||
|
SSLCloseOnNegotiateionFail=0
|
||||||
|
SSLAcceptServerSelfSignedCertificates=Yes
|
||||||
|
SSLAcceptServerUnverifiedCertificates=Yes
|
||||||
|
SSLDataChannelProtected=Yes
|
||||||
|
SSLCryptographicMode=1
|
||||||
|
AllowResume=No
|
||||||
|
EPSV4=0
|
||||||
|
[Stats]
|
||||||
|
NumVisits=0
|
||||||
122
playbook/shopfloor-setup/Shopfloor/csf/Profile/WJ_Office.hep
Executable file
122
playbook/shopfloor-setup/Shopfloor/csf/Profile/WJ_Office.hep
Executable file
@@ -0,0 +1,122 @@
|
|||||||
|
[PROFILE]
|
||||||
|
Session Terminal Type=2
|
||||||
|
Session Properties=Default VT
|
||||||
|
Menu=Default VT
|
||||||
|
Auto Run Macro Delay Time=1
|
||||||
|
Gateway Name=wjfms3.apps.wlm.geaerospace.net
|
||||||
|
Session Keep Alive Interval=600
|
||||||
|
PC Keyboard Type=5
|
||||||
|
Tabstops='8 16 24 32 40 48 56 64 72 '
|
||||||
|
VT BG Colors='1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 8 7 7 9 15 14 10 2 10 '
|
||||||
|
Default VT Recv Path=$MYDOCUMENTS$\
|
||||||
|
SSL/TLS Version=3
|
||||||
|
SSL/TLS Current User Certificate=1
|
||||||
|
Window Title=%s - %p (%h)
|
||||||
|
WinInfo Default Restored=0 0 0 0 22 22 892 619 0 0 0=20 10 0 0 400 0 0 0 0 3 2 2 0 HE_Bitmap| 0 0
|
||||||
|
WinInfo Default Maximized=0 0 0 0 -8 -8 1634 1066 0 0 0=30 15 0 0 700 0 0 0 1 3 2 2 0 HE_Bitmap| 0 0
|
||||||
|
Print Header2=Page: %# Document Name: %F
|
||||||
|
QuickKey=Super Key F6
|
||||||
|
Never Run Profile=Off
|
||||||
|
dwVTFlags1=29369344
|
||||||
|
VT FG Colors='7 7 9 15 14 10 2 10 0 1 2 3 4 5 15 7 8 9 10 11 12 13 14 15 15 0 0 1 0 0 0 0 0 0 0 '
|
||||||
|
Connect Timeout=60
|
||||||
|
Disable Secure Connection Warning=Yes
|
||||||
|
Profile File Version=200
|
||||||
|
Cryptographic Mode=1
|
||||||
|
Display RowCol Indicator=Off
|
||||||
|
VT Scrollback Save Attribs=Off
|
||||||
|
VT True Display Mode=1
|
||||||
|
Status Line=1
|
||||||
|
Kerberos Use Hummingbird Kerberos=On
|
||||||
|
Keyboard=Office
|
||||||
|
Auto Start Quick-Key=Office.ebs
|
||||||
|
On Disconnect=0
|
||||||
|
Allow sleep while connected=0
|
||||||
|
Toolbar Scheme Name=Default VT
|
||||||
|
Save Profile on Window Close=Off
|
||||||
|
Toolbar Scheme Modified=Default VT:30026745--1815298816
|
||||||
|
Save Toolbar Info on Exit=Off
|
||||||
|
Save Mode=0
|
||||||
|
[Session Options]
|
||||||
|
Last Selected Page=4000
|
||||||
|
[HEBar-59440]
|
||||||
|
[Toolbars Layout-Summary]
|
||||||
|
Bars=3
|
||||||
|
ScreenCX=1600
|
||||||
|
ScreenCY=1200
|
||||||
|
[Toolbars]
|
||||||
|
ToolBarVersion=5
|
||||||
|
bar0=Default VT ToolBar.tbv
|
||||||
|
[Toolbars Layout-Bar0]
|
||||||
|
BarID=59393
|
||||||
|
Visible=0
|
||||||
|
[Toolbars Layout-Bar1]
|
||||||
|
BarID=59419
|
||||||
|
Bars=3
|
||||||
|
Bar#0=0
|
||||||
|
Bar#1=59440
|
||||||
|
Bar#2=0
|
||||||
|
[Toolbars Layout-Bar2]
|
||||||
|
BarID=59440
|
||||||
|
XPos=-2
|
||||||
|
YPos=-2
|
||||||
|
Docking=1
|
||||||
|
MRUDockID=59419
|
||||||
|
MRUDockLeftPos=-2
|
||||||
|
MRUDockTopPos=-2
|
||||||
|
MRUDockRightPos=541
|
||||||
|
MRUDockBottomPos=26
|
||||||
|
MRUFloatStyle=8196
|
||||||
|
MRUFloatXPos=-2147483648
|
||||||
|
MRUFloatYPos=0
|
||||||
|
[Hotspots]
|
||||||
|
ShowTips=1
|
||||||
|
[ShortcutReplacers]
|
||||||
|
Size=0
|
||||||
|
[Site Info]
|
||||||
|
EnableCache=Yes
|
||||||
|
RefreshCacheOnConnect=0
|
||||||
|
AlwaysUploadUsing=0
|
||||||
|
AlwaysDownloadUsing=0
|
||||||
|
ProfileVersion=5
|
||||||
|
SecurityType=0
|
||||||
|
Port=21
|
||||||
|
SSL Implicit Port=990
|
||||||
|
Directory2=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
SFTPProxyAddress=localhost
|
||||||
|
SFTPProxyPort=0
|
||||||
|
SFTPWriteSize=256
|
||||||
|
GssApiServiceName=host
|
||||||
|
GssApiProtLevel=P
|
||||||
|
GssApiIssueCCC=No
|
||||||
|
GssApiPBSZ=16000
|
||||||
|
GssApiKerbClient=0
|
||||||
|
GssApiUseHMS2MIT=No
|
||||||
|
PassiveMode=2
|
||||||
|
AllowServer2ServerTransfer=0
|
||||||
|
KeepAlive=No
|
||||||
|
AutoDetectServerType=Yes
|
||||||
|
KeepAliveTime=0
|
||||||
|
FirewallPort=21
|
||||||
|
FirewallType=0
|
||||||
|
NetworkTimeout=120
|
||||||
|
ConvertGMTTime=0
|
||||||
|
ResolveLinks=1
|
||||||
|
RootToInitialDir=0
|
||||||
|
ChangeDirChangesRoot=0
|
||||||
|
DropVmsVersion=0
|
||||||
|
LocalHourOffset=0
|
||||||
|
LocalMinuteOffset=0
|
||||||
|
TemporaryProfile=0
|
||||||
|
SSLVersion=3
|
||||||
|
SSLReuse=0
|
||||||
|
SSLUserCertificateMode=0
|
||||||
|
SSLCloseOnNegotiateionFail=0
|
||||||
|
SSLAcceptServerSelfSignedCertificates=Yes
|
||||||
|
SSLAcceptServerUnverifiedCertificates=Yes
|
||||||
|
SSLDataChannelProtected=Yes
|
||||||
|
SSLCryptographicMode=1
|
||||||
|
AllowResume=No
|
||||||
|
EPSV4=0
|
||||||
|
[Stats]
|
||||||
|
NumVisits=0
|
||||||
128
playbook/shopfloor-setup/Shopfloor/csf/Profile/mmcs.hep
Executable file
128
playbook/shopfloor-setup/Shopfloor/csf/Profile/mmcs.hep
Executable file
@@ -0,0 +1,128 @@
|
|||||||
|
[PROFILE]
|
||||||
|
Session Terminal Type=2
|
||||||
|
Never Run Profile=Off
|
||||||
|
Session Properties=Default VT
|
||||||
|
Menu=Default VT
|
||||||
|
Auto Run Macro Delay Time=1
|
||||||
|
Gateway Name=wjfms3.apps.wlm.geaerospace.net
|
||||||
|
Session Keep Alive Interval=600
|
||||||
|
dwVTFlags1=29369344
|
||||||
|
PC Keyboard Type=5
|
||||||
|
Keyboard=ATM-VT420
|
||||||
|
Tabstops='8 16 24 32 40 48 56 64 72 80 88 96 104 112 120 128 136 144 152 160 168 176 184 192 200 208 216 224 232 240 248 256 264 272 280 288 296 '
|
||||||
|
Default VT Recv Path=$MYDOCUMENTS$\
|
||||||
|
SSL/TLS Version=3
|
||||||
|
SSL/TLS Current User Certificate=1
|
||||||
|
Window Title=%s - %p (%h)
|
||||||
|
WinInfo Default Restored=0 0 0 0 10 10 757 491 0 0 0=16 9 0 0 400 0 0 0 0 3 2 2 0 HE_Bitmap| 0 0
|
||||||
|
WinInfo Default Maximized=0 0 0 0 -8 -8 1382 744 0 0 0=26 13 0 0 400 0 0 0 0 3 2 2 0 HE_Bitmap| 0 0
|
||||||
|
Window State=2
|
||||||
|
Use Specific PRTSCR Printer=On
|
||||||
|
PrtScr DevMode=1025,1024,156,1497,33565,1,256,2794,1240,0,1,4,300,1,1,300,1,0,0,1,0,0,0,0,ZDesigner TLP 3842,User defined
|
||||||
|
PrtScr Driver=winspool
|
||||||
|
PrtScr Device=ZDesigner TLP 3842
|
||||||
|
PrtScr Output=LPT1:
|
||||||
|
Print Header2=Page: %# Document Name: %F
|
||||||
|
Use Specific VT Printer=On
|
||||||
|
VT DevMode=1025,1024,156,1497,323347,1,256,190,914,0,1,4,300,1,1,300,1,0,0,1,0,0,0,0,ZDesigner TLP 3842,User defined
|
||||||
|
VT Driver=winspool
|
||||||
|
VT Device=ZDesigner TLP 3842
|
||||||
|
VT Output=USB001
|
||||||
|
Printer Target=1
|
||||||
|
VT Printer Bypass Windows Print Driver=Off
|
||||||
|
VT Printer Use Default Font=On
|
||||||
|
VT Printer Margins Left=320
|
||||||
|
VT Printer Margins Right=0
|
||||||
|
VT Printer Margins Bottom=0
|
||||||
|
VT Printer Margins Top=0
|
||||||
|
QuickKey=ID
|
||||||
|
Auto Start Quick-Key=mmcs.ebs
|
||||||
|
VT Auto Wrap=On
|
||||||
|
Profile File Version=200
|
||||||
|
Kerberos Use Hummingbird Kerberos=On
|
||||||
|
Cryptographic Mode=1
|
||||||
|
Save Mode=0
|
||||||
|
[HEBar-59440]
|
||||||
|
sizeFloatCX=534
|
||||||
|
sizeFloatCY=28
|
||||||
|
[Toolbars Layout-Summary]
|
||||||
|
Bars=3
|
||||||
|
ScreenCX=1366
|
||||||
|
ScreenCY=768
|
||||||
|
[Session Options]
|
||||||
|
Last Selected Page=4000
|
||||||
|
[Toolbars]
|
||||||
|
ToolBarVersion=5
|
||||||
|
bar0=Default VT ToolBar.tbv
|
||||||
|
[Toolbars Layout-Bar0]
|
||||||
|
BarID=59393
|
||||||
|
[Toolbars Layout-Bar1]
|
||||||
|
BarID=59419
|
||||||
|
Bars=3
|
||||||
|
Bar#0=0
|
||||||
|
Bar#1=59440
|
||||||
|
Bar#2=0
|
||||||
|
[Toolbars Layout-Bar2]
|
||||||
|
BarID=59440
|
||||||
|
XPos=-2
|
||||||
|
YPos=-2
|
||||||
|
Docking=1
|
||||||
|
MRUDockID=59419
|
||||||
|
MRUDockLeftPos=-2
|
||||||
|
MRUDockTopPos=-2
|
||||||
|
MRUDockRightPos=541
|
||||||
|
MRUDockBottomPos=26
|
||||||
|
MRUFloatStyle=8196
|
||||||
|
MRUFloatXPos=-1
|
||||||
|
MRUFloatYPos=0
|
||||||
|
[Hotspots]
|
||||||
|
ShowTips=1
|
||||||
|
[ShortcutReplacers]
|
||||||
|
Size=0
|
||||||
|
[Site Info]
|
||||||
|
EnableCache=Yes
|
||||||
|
RefreshCacheOnConnect=0
|
||||||
|
AlwaysUploadUsing=0
|
||||||
|
AlwaysDownloadUsing=0
|
||||||
|
ProfileVersion=5
|
||||||
|
SecurityType=0
|
||||||
|
Port=21
|
||||||
|
SSL Implicit Port=990
|
||||||
|
Directory2=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
SFTPProxyAddress=localhost
|
||||||
|
SFTPProxyPort=0
|
||||||
|
SFTPWriteSize=256
|
||||||
|
GssApiServiceName=host
|
||||||
|
GssApiProtLevel=P
|
||||||
|
GssApiIssueCCC=No
|
||||||
|
GssApiPBSZ=16000
|
||||||
|
GssApiKerbClient=0
|
||||||
|
GssApiUseHMS2MIT=No
|
||||||
|
PassiveMode=2
|
||||||
|
AllowServer2ServerTransfer=0
|
||||||
|
KeepAlive=No
|
||||||
|
AutoDetectServerType=Yes
|
||||||
|
KeepAliveTime=0
|
||||||
|
FirewallPort=21
|
||||||
|
FirewallType=0
|
||||||
|
NetworkTimeout=120
|
||||||
|
ConvertGMTTime=0
|
||||||
|
ResolveLinks=1
|
||||||
|
RootToInitialDir=0
|
||||||
|
ChangeDirChangesRoot=0
|
||||||
|
DropVmsVersion=0
|
||||||
|
LocalHourOffset=0
|
||||||
|
LocalMinuteOffset=0
|
||||||
|
TemporaryProfile=0
|
||||||
|
SSLVersion=1
|
||||||
|
SSLReuse=0
|
||||||
|
SSLUserCertificateMode=0
|
||||||
|
SSLCloseOnNegotiateionFail=0
|
||||||
|
SSLAcceptServerSelfSignedCertificates=Yes
|
||||||
|
SSLAcceptServerUnverifiedCertificates=Yes
|
||||||
|
SSLDataChannelProtected=Yes
|
||||||
|
SSLCryptographicMode=1
|
||||||
|
AllowResume=No
|
||||||
|
EPSV4=0
|
||||||
|
[Stats]
|
||||||
|
NumVisits=0
|
||||||
BIN
playbook/shopfloor-setup/Shopfloor/csf/WJ Shopfloor.lnk
Executable file
BIN
playbook/shopfloor-setup/Shopfloor/csf/WJ Shopfloor.lnk
Executable file
Binary file not shown.
BIN
playbook/shopfloor-setup/Shopfloor/csf/WJ_Office.lnk
Executable file
BIN
playbook/shopfloor-setup/Shopfloor/csf/WJ_Office.lnk
Executable file
Binary file not shown.
BIN
playbook/shopfloor-setup/Shopfloor/csf/mmcs.lnk
Executable file
BIN
playbook/shopfloor-setup/Shopfloor/csf/mmcs.lnk
Executable file
Binary file not shown.
84
playbook/shopfloor-setup/Standard/01-eDNC.ps1
Normal file
84
playbook/shopfloor-setup/Standard/01-eDNC.ps1
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# 01-eDNC.ps1 — Install eDNC and MarkZebra, deploy custom eMxInfo.txt (Standard)
|
||||||
|
|
||||||
|
Write-Host "=== eDNC / MarkZebra Setup ==="
|
||||||
|
|
||||||
|
$edncDir = "C:\Enrollment\shopfloor-setup\Standard\eDNC"
|
||||||
|
|
||||||
|
if (-not (Test-Path $edncDir)) {
|
||||||
|
Write-Warning "eDNC folder not found at $edncDir — skipping."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Find installers ---
|
||||||
|
$edncMsi = Get-ChildItem -Path $edncDir -Filter "eDNC-*.msi" | Select-Object -First 1
|
||||||
|
$markMsi = Get-ChildItem -Path $edncDir -Filter "MarkZebra.msi" | Select-Object -First 1
|
||||||
|
$emxInfo = Join-Path $edncDir "eMxInfo.txt"
|
||||||
|
|
||||||
|
# --- 1. Install eDNC ---
|
||||||
|
if ($edncMsi) {
|
||||||
|
Write-Host "Installing eDNC: $($edncMsi.Name)..."
|
||||||
|
$p = Start-Process -FilePath "msiexec.exe" -ArgumentList "/i `"$($edncMsi.FullName)`" /qn /norestart LAUNCHNTLARS=false" -Wait -PassThru
|
||||||
|
Write-Host " eDNC exit code: $($p.ExitCode)"
|
||||||
|
} else {
|
||||||
|
Write-Warning "eDNC installer not found in $edncDir (expected eDNC-*.msi)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 2. Install MarkZebra ---
|
||||||
|
if ($markMsi) {
|
||||||
|
Write-Host "Installing MarkZebra: $($markMsi.Name)..."
|
||||||
|
$p = Start-Process -FilePath "msiexec.exe" -ArgumentList "/i `"$($markMsi.FullName)`" /qn /norestart LAUNCHNTLARS=false" -Wait -PassThru
|
||||||
|
Write-Host " MarkZebra exit code: $($p.ExitCode)"
|
||||||
|
} else {
|
||||||
|
Write-Warning "MarkZebra installer not found in $edncDir (expected MarkZebra.msi)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 3. Mirror x86 installs to 64-bit Program Files (app uses hardcoded paths) ---
|
||||||
|
# MarkZebra.exe references \Mark\, mxTransactionDll.dll references \Dnc\Server Files\
|
||||||
|
$copies = @(
|
||||||
|
@{ Src = "C:\Program Files (x86)\Mark"; Dst = "C:\Program Files\Mark" },
|
||||||
|
@{ Src = "C:\Program Files (x86)\Dnc"; Dst = "C:\Program Files\Dnc" }
|
||||||
|
)
|
||||||
|
foreach ($c in $copies) {
|
||||||
|
if (Test-Path $c.Src) {
|
||||||
|
if (-not (Test-Path $c.Dst)) {
|
||||||
|
New-Item -Path $c.Dst -ItemType Directory -Force | Out-Null
|
||||||
|
}
|
||||||
|
Copy-Item -Path "$($c.Src)\*" -Destination $c.Dst -Recurse -Force
|
||||||
|
Write-Host " Copied $($c.Src) -> $($c.Dst)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 4. Set DNC site and MarkZebra config ---
|
||||||
|
$regBase = "HKLM\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC"
|
||||||
|
reg add "$regBase\General" /v Site /t REG_SZ /d WestJefferson /f | Out-Null
|
||||||
|
Write-Host " DNC site set to WestJefferson."
|
||||||
|
|
||||||
|
reg add "$regBase\Mark" /v "Port Id" /t REG_SZ /d COM1 /f | Out-Null
|
||||||
|
reg add "$regBase\Mark" /v "Baud" /t REG_SZ /d 9600 /f | Out-Null
|
||||||
|
reg add "$regBase\Mark" /v "Parity" /t REG_SZ /d None /f | Out-Null
|
||||||
|
reg add "$regBase\Mark" /v "Data Bits" /t REG_SZ /d 8 /f | Out-Null
|
||||||
|
reg add "$regBase\Mark" /v "Stop Bits" /t REG_SZ /d 1 /f | Out-Null
|
||||||
|
reg add "$regBase\Mark" /v "Message Type" /t REG_SZ /d V /f | Out-Null
|
||||||
|
reg add "$regBase\Mark" /v "Debug" /t REG_SZ /d ON /f | Out-Null
|
||||||
|
reg add "$regBase\Mark" /v "MarkerType" /t REG_SZ /d Mark2D /f | Out-Null
|
||||||
|
reg add "$regBase\Mark" /v "DncPatterns" /t REG_SZ /d NO /f | Out-Null
|
||||||
|
Set-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\Mark" -Name "CageCode" -Value "" -Force
|
||||||
|
Write-Host " MarkZebra registry configured."
|
||||||
|
|
||||||
|
# --- 5. Deploy custom eMxInfo.txt to both Program Files paths ---
|
||||||
|
if (Test-Path $emxInfo) {
|
||||||
|
$dest86 = "C:\Program Files (x86)\DNC\Server Files"
|
||||||
|
$dest64 = "C:\Program Files\DNC\Server Files"
|
||||||
|
|
||||||
|
foreach ($dest in @($dest86, $dest64)) {
|
||||||
|
if (-not (Test-Path $dest)) {
|
||||||
|
New-Item -Path $dest -ItemType Directory -Force | Out-Null
|
||||||
|
}
|
||||||
|
Copy-Item -Path $emxInfo -Destination (Join-Path $dest "eMxInfo.txt") -Force
|
||||||
|
Write-Host " eMxInfo.txt -> $dest"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Warning "eMxInfo.txt not found at $emxInfo"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "=== eDNC / MarkZebra Setup Complete ==="
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# 01-Setup-WaxAndTrace.ps1 — Wax and Trace-specific setup (runs after Shopfloor baseline)
|
||||||
|
|
||||||
|
Write-Host "=== Wax and Trace Setup ==="
|
||||||
|
|
||||||
|
# --- Add Wax and Trace credentials ---
|
||||||
|
# cmdkey /generic:wax-server /user:domain\waxuser /pass:password
|
||||||
|
|
||||||
|
# --- Install Wax and Trace applications ---
|
||||||
|
# Start-Process msiexec.exe -ArgumentList '/i "C:\Enrollment\shopfloor-setup\WaxAndTrace\WaxApp.msi" /qn' -Wait
|
||||||
|
|
||||||
|
# --- Wax and Trace configuration ---
|
||||||
|
# Set-ItemProperty -Path "HKLM:\SOFTWARE\CompanyName" -Name "PCType" -Value "WaxAndTrace"
|
||||||
|
|
||||||
|
Write-Host "=== Wax and Trace Setup Complete ==="
|
||||||
54
playbook/shopfloor-setup/backup_lockdown.bat
Normal file
54
playbook/shopfloor-setup/backup_lockdown.bat
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
@echo off
|
||||||
|
title Shopfloor Backup Lockdown
|
||||||
|
|
||||||
|
:: Self-elevate to administrator
|
||||||
|
net session >nul 2>&1
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo Requesting administrator privileges...
|
||||||
|
powershell -Command "Start-Process '%~f0' -Verb RunAs"
|
||||||
|
exit /b
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo Shopfloor Backup Lockdown
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: Run SFLD autologon script first
|
||||||
|
echo Running SFLD autologon script...
|
||||||
|
"C:\Program Files\PowerShell\7\pwsh.exe" -NoProfile -ExecutionPolicy Bypass -File "C:\Program Files\Sysinternals\sfld_autologon.ps1"
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Waiting 10 seconds...
|
||||||
|
ping -n 11 127.0.0.1 >nul
|
||||||
|
|
||||||
|
:: Discover the EnterpriseMgmt enrollment GUID
|
||||||
|
for /f "delims=" %%G in ('powershell -NoProfile -Command "$t = Get-ScheduledTask | Where-Object { $_.TaskPath -match '\\Microsoft\\EnterpriseMgmt\\' -and $_.TaskName -match 'Schedule #1' }; if ($t) { $t.TaskPath -replace '.*EnterpriseMgmt\\([^\\]+)\\.*','$1' | Select-Object -First 1 } else { '' }"') do set GUID=%%G
|
||||||
|
|
||||||
|
if not defined GUID (
|
||||||
|
echo ERROR: No EnterpriseMgmt enrollment GUID found.
|
||||||
|
echo The device may not be enrolled in MDM yet.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Enrollment GUID: %GUID%
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo Running EnterpriseMgmt Schedule #1...
|
||||||
|
schtasks /run /tn "\Microsoft\EnterpriseMgmt\%GUID%\Schedule #1 created by enrollment client"
|
||||||
|
echo Waiting 30 seconds...
|
||||||
|
ping -n 31 127.0.0.1 >nul
|
||||||
|
|
||||||
|
echo Running EnterpriseMgmt Schedule #2...
|
||||||
|
schtasks /run /tn "\Microsoft\EnterpriseMgmt\%GUID%\Schedule #2 created by enrollment client"
|
||||||
|
echo Waiting 90 seconds...
|
||||||
|
ping -n 91 127.0.0.1 >nul
|
||||||
|
|
||||||
|
echo Running EnterpriseMgmt Schedule #3...
|
||||||
|
schtasks /run /tn "\Microsoft\EnterpriseMgmt\%GUID%\Schedule #3 created by enrollment client"
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Lockdown complete.
|
||||||
|
pause
|
||||||
@@ -86,7 +86,7 @@ echo 2. Wax and Trace
|
|||||||
echo 3. Keyence
|
echo 3. Keyence
|
||||||
echo 4. Genspect
|
echo 4. Genspect
|
||||||
echo 5. Display
|
echo 5. Display
|
||||||
echo 6. Shopfloor (General)
|
echo 6. Standard
|
||||||
echo.
|
echo.
|
||||||
set PCTYPE=
|
set PCTYPE=
|
||||||
set /p pctype_choice=Enter your choice (1-6):
|
set /p pctype_choice=Enter your choice (1-6):
|
||||||
@@ -95,7 +95,7 @@ if "%pctype_choice%"=="2" set PCTYPE=WaxAndTrace
|
|||||||
if "%pctype_choice%"=="3" set PCTYPE=Keyence
|
if "%pctype_choice%"=="3" set PCTYPE=Keyence
|
||||||
if "%pctype_choice%"=="4" set PCTYPE=Genspect
|
if "%pctype_choice%"=="4" set PCTYPE=Genspect
|
||||||
if "%pctype_choice%"=="5" set PCTYPE=Display
|
if "%pctype_choice%"=="5" set PCTYPE=Display
|
||||||
if "%pctype_choice%"=="6" set PCTYPE=Shopfloor
|
if "%pctype_choice%"=="6" set PCTYPE=Standard
|
||||||
if "%PCTYPE%"=="" goto pctype_menu
|
if "%PCTYPE%"=="" goto pctype_menu
|
||||||
|
|
||||||
REM --- Display sub-type selection ---
|
REM --- Display sub-type selection ---
|
||||||
@@ -245,13 +245,13 @@ if not "%DISPLAYTYPE%"=="" echo %DISPLAYTYPE%> W:\Enrollment\display-type.txt
|
|||||||
copy /Y "Y:\shopfloor-setup\Run-ShopfloorSetup.ps1" "W:\Enrollment\Run-ShopfloorSetup.ps1"
|
copy /Y "Y:\shopfloor-setup\Run-ShopfloorSetup.ps1" "W:\Enrollment\Run-ShopfloorSetup.ps1"
|
||||||
REM --- Always copy Shopfloor baseline scripts ---
|
REM --- Always copy Shopfloor baseline scripts ---
|
||||||
mkdir W:\Enrollment\shopfloor-setup 2>NUL
|
mkdir W:\Enrollment\shopfloor-setup 2>NUL
|
||||||
|
copy /Y "Y:\shopfloor-setup\backup_lockdown.bat" "W:\Enrollment\shopfloor-setup\backup_lockdown.bat"
|
||||||
if exist "Y:\shopfloor-setup\Shopfloor" (
|
if exist "Y:\shopfloor-setup\Shopfloor" (
|
||||||
mkdir W:\Enrollment\shopfloor-setup\Shopfloor 2>NUL
|
mkdir W:\Enrollment\shopfloor-setup\Shopfloor 2>NUL
|
||||||
xcopy /E /Y /I "Y:\shopfloor-setup\Shopfloor" "W:\Enrollment\shopfloor-setup\Shopfloor\"
|
xcopy /E /Y /I "Y:\shopfloor-setup\Shopfloor" "W:\Enrollment\shopfloor-setup\Shopfloor\"
|
||||||
echo Copied Shopfloor baseline setup files.
|
echo Copied Shopfloor baseline setup files.
|
||||||
)
|
)
|
||||||
REM --- Copy type-specific scripts on top of baseline ---
|
REM --- Copy type-specific scripts on top of baseline ---
|
||||||
if "%PCTYPE%"=="Shopfloor" goto pctype_done
|
|
||||||
if exist "Y:\shopfloor-setup\%PCTYPE%" (
|
if exist "Y:\shopfloor-setup\%PCTYPE%" (
|
||||||
mkdir "W:\Enrollment\shopfloor-setup\%PCTYPE%" 2>NUL
|
mkdir "W:\Enrollment\shopfloor-setup\%PCTYPE%" 2>NUL
|
||||||
xcopy /E /Y /I "Y:\shopfloor-setup\%PCTYPE%" "W:\Enrollment\shopfloor-setup\%PCTYPE%\"
|
xcopy /E /Y /I "Y:\shopfloor-setup\%PCTYPE%" "W:\Enrollment\shopfloor-setup\%PCTYPE%\"
|
||||||
|
|||||||
177
sync_hardware_models.py
Normal file
177
sync_hardware_models.py
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Sync HardwareDriver.json and user_selections.json across all PXE image types.
|
||||||
|
|
||||||
|
Reads all HardwareDriver.json files, builds a unified driver catalog,
|
||||||
|
then updates each image to include all known hardware models.
|
||||||
|
Run after adding new driver packs to the shared Out-of-box Drivers directory.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
WINPEAPPS = Path("/srv/samba/winpeapps")
|
||||||
|
SHARED_DRIVERS = WINPEAPPS / "_shared" / "Out-of-box Drivers"
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_entry(entry):
|
||||||
|
"""Normalize a HardwareDriver.json entry to a consistent format."""
|
||||||
|
norm = {}
|
||||||
|
norm["manufacturer"] = entry.get("manufacturer", "Dell")
|
||||||
|
norm["product"] = entry.get("product") or entry.get("manufacturerfriendlyname", "Dell")
|
||||||
|
norm["family"] = entry.get("family", "")
|
||||||
|
norm["modelswminame"] = entry.get("modelswminame") or entry.get("models", "")
|
||||||
|
norm["modelsfriendlyname"] = entry.get("modelsfriendlyname", "")
|
||||||
|
norm["fileName"] = entry.get("fileName") or entry.get("FileName", "")
|
||||||
|
norm["destinationDir"] = entry.get("destinationDir") or entry.get("DestinationDir", "")
|
||||||
|
norm["url"] = entry.get("url", "")
|
||||||
|
norm["hash"] = entry.get("hash", "")
|
||||||
|
norm["size"] = entry.get("size", 0)
|
||||||
|
norm["modifiedDate"] = entry.get("modifiedDate", "0001-01-01T00:00:00")
|
||||||
|
norm["osId"] = entry.get("osId", "")
|
||||||
|
norm["imagedisk"] = entry.get("imagedisk", 0)
|
||||||
|
return norm
|
||||||
|
|
||||||
|
|
||||||
|
def merge_os_ids(a, b):
|
||||||
|
"""Merge two osId strings (e.g., '18' + '20,21' -> '18,20,21')."""
|
||||||
|
ids = set()
|
||||||
|
for oid in [a, b]:
|
||||||
|
for part in str(oid).split(","):
|
||||||
|
part = part.strip()
|
||||||
|
if part:
|
||||||
|
ids.add(part)
|
||||||
|
return ",".join(sorted(ids, key=lambda x: int(x) if x.isdigit() else 0))
|
||||||
|
|
||||||
|
|
||||||
|
def check_driver_exists(entry):
|
||||||
|
"""Check if the driver zip actually exists in the shared directory."""
|
||||||
|
dest = entry["destinationDir"]
|
||||||
|
dest = dest.replace("*destinationdir*", "")
|
||||||
|
dest = dest.lstrip("\\")
|
||||||
|
dest = dest.replace("\\", "/")
|
||||||
|
# Strip leading path components that are already in SHARED_DRIVERS
|
||||||
|
for prefix in ["Deploy/Out-of-box Drivers/", "Out-of-box Drivers/"]:
|
||||||
|
if dest.startswith(prefix):
|
||||||
|
dest = dest[len(prefix):]
|
||||||
|
break
|
||||||
|
dest = dest.lstrip("/")
|
||||||
|
zip_path = SHARED_DRIVERS / dest / entry["fileName"]
|
||||||
|
return zip_path.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=== PXE Hardware Model Sync ===")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Step 1: Build unified catalog from all images
|
||||||
|
print("Reading driver catalogs...")
|
||||||
|
catalog = OrderedDict()
|
||||||
|
|
||||||
|
image_dirs = sorted(
|
||||||
|
[d for d in WINPEAPPS.iterdir() if d.is_dir() and not d.name.startswith("_")]
|
||||||
|
)
|
||||||
|
|
||||||
|
for img_dir in image_dirs:
|
||||||
|
hw_file = img_dir / "Deploy" / "Control" / "HardwareDriver.json"
|
||||||
|
if not hw_file.exists():
|
||||||
|
continue
|
||||||
|
with open(hw_file) as f:
|
||||||
|
entries = json.load(f)
|
||||||
|
print(" Read {} entries from {}".format(len(entries), img_dir.name))
|
||||||
|
for entry in entries:
|
||||||
|
norm = normalize_entry(entry)
|
||||||
|
key = (norm["family"], norm["fileName"])
|
||||||
|
if key in catalog:
|
||||||
|
catalog[key]["osId"] = merge_os_ids(
|
||||||
|
catalog[key]["osId"], norm["osId"]
|
||||||
|
)
|
||||||
|
# Prefer longer/more complete model names
|
||||||
|
if len(norm["modelswminame"]) > len(catalog[key]["modelswminame"]):
|
||||||
|
catalog[key]["modelswminame"] = norm["modelswminame"]
|
||||||
|
if len(norm["modelsfriendlyname"]) > len(
|
||||||
|
catalog[key]["modelsfriendlyname"]
|
||||||
|
):
|
||||||
|
catalog[key]["modelsfriendlyname"] = norm["modelsfriendlyname"]
|
||||||
|
else:
|
||||||
|
catalog[key] = norm
|
||||||
|
|
||||||
|
unified = list(catalog.values())
|
||||||
|
print()
|
||||||
|
print("Unified catalog: {} unique driver entries".format(len(unified)))
|
||||||
|
|
||||||
|
# Step 2: Check which drivers actually exist on disk
|
||||||
|
missing = []
|
||||||
|
found = 0
|
||||||
|
for entry in unified:
|
||||||
|
if check_driver_exists(entry):
|
||||||
|
found += 1
|
||||||
|
else:
|
||||||
|
missing.append(
|
||||||
|
" {}: {}".format(entry["family"], entry["fileName"])
|
||||||
|
)
|
||||||
|
|
||||||
|
print(" {} drivers found on disk".format(found))
|
||||||
|
if missing:
|
||||||
|
print(" WARNING: {} driver zips NOT found on disk:".format(len(missing)))
|
||||||
|
for m in missing[:15]:
|
||||||
|
print(m)
|
||||||
|
if len(missing) > 15:
|
||||||
|
print(" ... and {} more".format(len(missing) - 15))
|
||||||
|
print(" (Entries still included - PESetup may download them)")
|
||||||
|
|
||||||
|
# Step 3: Build unified model selection from all driver entries
|
||||||
|
models = []
|
||||||
|
seen = set()
|
||||||
|
for entry in unified:
|
||||||
|
friendly_names = [
|
||||||
|
n.strip()
|
||||||
|
for n in entry["modelsfriendlyname"].split(",")
|
||||||
|
if n.strip()
|
||||||
|
]
|
||||||
|
family = entry["family"]
|
||||||
|
for name in friendly_names:
|
||||||
|
key = (name, family)
|
||||||
|
if key not in seen:
|
||||||
|
seen.add(key)
|
||||||
|
models.append({"Model": name, "Id": family})
|
||||||
|
models.sort(key=lambda x: x["Model"])
|
||||||
|
print()
|
||||||
|
print("Unified model selection: {} models".format(len(models)))
|
||||||
|
|
||||||
|
# Step 4: Update each image
|
||||||
|
print()
|
||||||
|
print("Updating images...")
|
||||||
|
for img_dir in image_dirs:
|
||||||
|
hw_file = img_dir / "Deploy" / "Control" / "HardwareDriver.json"
|
||||||
|
us_file = img_dir / "Tools" / "user_selections.json"
|
||||||
|
if not hw_file.exists() or not us_file.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Write unified HardwareDriver.json
|
||||||
|
with open(hw_file, "w") as f:
|
||||||
|
json.dump(unified, f, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
|
# Update user_selections.json (preserve OperatingSystemSelection etc.)
|
||||||
|
with open(us_file) as f:
|
||||||
|
user_sel = json.load(f)
|
||||||
|
old_count = len(user_sel[0].get("HardwareModelSelection", []))
|
||||||
|
user_sel[0]["HardwareModelSelection"] = models
|
||||||
|
with open(us_file, "w") as f:
|
||||||
|
json.dump(user_sel, f, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
|
print(
|
||||||
|
" {}: {} -> {} models, {} driver entries".format(
|
||||||
|
img_dir.name, old_count, len(models), len(unified)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("Done!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
497
webapp/app.py
497
webapp/app.py
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Flask web application for managing a GE Aerospace PXE server."""
|
"""Flask web application for managing a GE Aerospace PXE server."""
|
||||||
|
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
@@ -51,14 +52,15 @@ def audit(action, detail=""):
|
|||||||
SAMBA_SHARE = os.environ.get("SAMBA_SHARE", "/srv/samba/winpeapps")
|
SAMBA_SHARE = os.environ.get("SAMBA_SHARE", "/srv/samba/winpeapps")
|
||||||
CLONEZILLA_SHARE = os.environ.get("CLONEZILLA_SHARE", "/srv/samba/clonezilla")
|
CLONEZILLA_SHARE = os.environ.get("CLONEZILLA_SHARE", "/srv/samba/clonezilla")
|
||||||
BLANCCO_REPORTS = os.environ.get("BLANCCO_REPORTS", "/srv/samba/blancco-reports")
|
BLANCCO_REPORTS = os.environ.get("BLANCCO_REPORTS", "/srv/samba/blancco-reports")
|
||||||
|
ENROLLMENT_SHARE = os.environ.get("ENROLLMENT_SHARE", "/srv/samba/enrollment")
|
||||||
UPLOAD_DIR = os.environ.get("UPLOAD_DIR", "/home/pxe/image-upload")
|
UPLOAD_DIR = os.environ.get("UPLOAD_DIR", "/home/pxe/image-upload")
|
||||||
SHARED_DIR = os.path.join(SAMBA_SHARE, "_shared")
|
SHARED_DIR = os.path.join(SAMBA_SHARE, "_shared")
|
||||||
# Subdirs inside Deploy/ shared across ALL image types
|
# Subdirs inside Deploy/ shared across ALL image types
|
||||||
SHARED_DEPLOY_GLOBAL = ["Out-of-box Drivers"]
|
SHARED_DEPLOY_GLOBAL = ["Out-of-box Drivers"]
|
||||||
# Subdirs inside Deploy/ shared within the same image family (by prefix)
|
# Subdirs inside Deploy/ shared within the same image family (by prefix)
|
||||||
SHARED_DEPLOY_SCOPED = {
|
SHARED_DEPLOY_SCOPED = {
|
||||||
"gea-": ["Operating Systems"],
|
"gea-": ["Operating Systems", "Packages"],
|
||||||
"ge-": ["Operating Systems"],
|
"ge-": ["Operating Systems", "Packages"],
|
||||||
}
|
}
|
||||||
# Sibling dirs at image root shared within the same image family
|
# Sibling dirs at image root shared within the same image family
|
||||||
SHARED_ROOT_DIRS = {
|
SHARED_ROOT_DIRS = {
|
||||||
@@ -149,6 +151,164 @@ def unattend_path(image_type):
|
|||||||
return os.path.join(deploy_path(image_type), "FlatUnattendW10.xml")
|
return os.path.join(deploy_path(image_type), "FlatUnattendW10.xml")
|
||||||
|
|
||||||
|
|
||||||
|
def control_path(image_type):
|
||||||
|
"""Return the Deploy/Control directory for an image type."""
|
||||||
|
return os.path.join(deploy_path(image_type), "Control")
|
||||||
|
|
||||||
|
|
||||||
|
def tools_path(image_type):
|
||||||
|
"""Return the Tools directory for an image type."""
|
||||||
|
return os.path.join(SAMBA_SHARE, image_type, "Tools")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_json(filepath):
|
||||||
|
"""Parse a JSON file and return its contents, or [] on failure."""
|
||||||
|
try:
|
||||||
|
with open(filepath, "r", encoding="utf-8-sig") as fh:
|
||||||
|
return json.load(fh)
|
||||||
|
except (OSError, json.JSONDecodeError):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _save_json(filepath, data):
|
||||||
|
"""Write data as pretty-printed JSON."""
|
||||||
|
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||||
|
with open(filepath, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(data, fh, indent=2, ensure_ascii=False)
|
||||||
|
fh.write("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_destination(dest_dir, image_type):
|
||||||
|
"""Convert a Windows *destinationdir* path to a Linux filesystem path.
|
||||||
|
|
||||||
|
Replaces the ``*destinationdir*`` placeholder and backslashes, then
|
||||||
|
prepends ``SAMBA_SHARE/image_type/`` and resolves symlinks.
|
||||||
|
"""
|
||||||
|
if not dest_dir:
|
||||||
|
return ""
|
||||||
|
# Replace the placeholder (case-insensitive)
|
||||||
|
path = dest_dir
|
||||||
|
lower = path.lower()
|
||||||
|
idx = lower.find("*destinationdir*")
|
||||||
|
if idx != -1:
|
||||||
|
path = path[idx + len("*destinationdir*"):]
|
||||||
|
# Backslash → forward slash, strip leading slash
|
||||||
|
path = path.replace("\\", "/").lstrip("/")
|
||||||
|
full = os.path.join(SAMBA_SHARE, image_type, path)
|
||||||
|
# Resolve symlinks so shared dirs are found
|
||||||
|
try:
|
||||||
|
full = os.path.realpath(full)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return full
|
||||||
|
|
||||||
|
|
||||||
|
def _load_image_config(image_type):
|
||||||
|
"""Load all JSON configs for an image and check on-disk presence."""
|
||||||
|
ctrl = control_path(image_type)
|
||||||
|
tools = tools_path(image_type)
|
||||||
|
|
||||||
|
# --- Drivers (merge HardwareDriver.json + hw_drivers.json) ---
|
||||||
|
hw_driver_file = os.path.join(ctrl, "HardwareDriver.json")
|
||||||
|
hw_drivers_extra = os.path.join(ctrl, "hw_drivers.json")
|
||||||
|
drivers_raw = _load_json(hw_driver_file)
|
||||||
|
extra_raw = _load_json(hw_drivers_extra)
|
||||||
|
|
||||||
|
# Merge: dedup by FileName (case-insensitive)
|
||||||
|
seen_files = set()
|
||||||
|
drivers = []
|
||||||
|
for d in drivers_raw + extra_raw:
|
||||||
|
fname = (d.get("FileName") or d.get("fileName") or "").lower()
|
||||||
|
if fname and fname in seen_files:
|
||||||
|
continue
|
||||||
|
if fname:
|
||||||
|
seen_files.add(fname)
|
||||||
|
drivers.append(d)
|
||||||
|
|
||||||
|
# Check disk presence for each driver
|
||||||
|
for d in drivers:
|
||||||
|
fname = d.get("FileName") or d.get("fileName") or ""
|
||||||
|
dest = d.get("DestinationDir") or d.get("destinationDir") or ""
|
||||||
|
resolved = _resolve_destination(dest, image_type)
|
||||||
|
if resolved and fname:
|
||||||
|
d["_on_disk"] = os.path.isfile(os.path.join(resolved, fname))
|
||||||
|
else:
|
||||||
|
d["_on_disk"] = False
|
||||||
|
|
||||||
|
# --- Operating Systems ---
|
||||||
|
os_file = os.path.join(ctrl, "OperatingSystem.json")
|
||||||
|
operating_systems = _load_json(os_file)
|
||||||
|
for entry in operating_systems:
|
||||||
|
osv = entry.get("operatingSystemVersion", {})
|
||||||
|
wim = osv.get("wim", {})
|
||||||
|
dest = wim.get("DestinationDir") or wim.get("destinationDir") or ""
|
||||||
|
resolved = _resolve_destination(dest, image_type)
|
||||||
|
if resolved:
|
||||||
|
entry["_on_disk"] = os.path.isfile(os.path.join(resolved, "install.wim"))
|
||||||
|
else:
|
||||||
|
entry["_on_disk"] = False
|
||||||
|
|
||||||
|
# --- Packages ---
|
||||||
|
pkg_file = os.path.join(ctrl, "packages.json")
|
||||||
|
packages = _load_json(pkg_file)
|
||||||
|
for p in packages:
|
||||||
|
fname = p.get("fileName") or p.get("FileName") or ""
|
||||||
|
dest = p.get("destinationDir") or p.get("DestinationDir") or ""
|
||||||
|
resolved = _resolve_destination(dest, image_type)
|
||||||
|
if resolved and fname:
|
||||||
|
p["_on_disk"] = os.path.isfile(os.path.join(resolved, fname))
|
||||||
|
else:
|
||||||
|
p["_on_disk"] = False
|
||||||
|
|
||||||
|
# --- Hardware Models (user_selections.json) ---
|
||||||
|
us_file = os.path.join(tools, "user_selections.json")
|
||||||
|
us_raw = _load_json(us_file)
|
||||||
|
us_data = us_raw[0] if us_raw and isinstance(us_raw, list) else {}
|
||||||
|
hardware_models = us_data.get("HardwareModelSelection", [])
|
||||||
|
os_selection = str(us_data.get("OperatingSystemSelection", ""))
|
||||||
|
|
||||||
|
# Build a lookup: family → driver entry (for disk-presence on models)
|
||||||
|
family_lookup = {}
|
||||||
|
for d in drivers:
|
||||||
|
family = d.get("family", "")
|
||||||
|
if family:
|
||||||
|
family_lookup[family] = d
|
||||||
|
|
||||||
|
for hm in hardware_models:
|
||||||
|
family_id = hm.get("Id", "")
|
||||||
|
matched = family_lookup.get(family_id)
|
||||||
|
hm["_on_disk"] = matched["_on_disk"] if matched else False
|
||||||
|
|
||||||
|
# --- Orphan drivers: zip files on disk not referenced in any JSON ---
|
||||||
|
orphan_drivers = []
|
||||||
|
oob_dir = os.path.join(deploy_path(image_type), "Out-of-box Drivers")
|
||||||
|
# Resolve symlinks
|
||||||
|
try:
|
||||||
|
oob_dir = os.path.realpath(oob_dir)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
registered_files = set()
|
||||||
|
for d in drivers:
|
||||||
|
fname = d.get("FileName") or d.get("fileName") or ""
|
||||||
|
if fname:
|
||||||
|
registered_files.add(fname.lower())
|
||||||
|
if os.path.isdir(oob_dir):
|
||||||
|
for dirpath, _dirnames, filenames in os.walk(oob_dir):
|
||||||
|
for fn in filenames:
|
||||||
|
if fn.lower().endswith(".zip") and fn.lower() not in registered_files:
|
||||||
|
rel = os.path.relpath(os.path.join(dirpath, fn), oob_dir)
|
||||||
|
orphan_drivers.append({"fileName": fn, "relPath": rel})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"hardware_models": hardware_models,
|
||||||
|
"drivers": drivers,
|
||||||
|
"operating_systems": operating_systems,
|
||||||
|
"packages": packages,
|
||||||
|
"orphan_drivers": orphan_drivers,
|
||||||
|
"os_selection": os_selection,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def image_status(image_type):
|
def image_status(image_type):
|
||||||
"""Return a dict describing the state of an image type."""
|
"""Return a dict describing the state of an image type."""
|
||||||
dp = deploy_path(image_type)
|
dp = deploy_path(image_type)
|
||||||
@@ -255,9 +415,10 @@ def _import_deploy(src_deploy, dst_deploy, target="", move=False):
|
|||||||
_replace_with_symlink(dst_item, shared_dest)
|
_replace_with_symlink(dst_item, shared_dest)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Normal transfer
|
# Normal transfer — merge to preserve existing custom content
|
||||||
if os.path.exists(dst_item):
|
if os.path.isdir(dst_item):
|
||||||
shutil.rmtree(dst_item)
|
_merge_tree(src_item, dst_item, move=move)
|
||||||
|
else:
|
||||||
_transfer_tree(src_item, dst_item)
|
_transfer_tree(src_item, dst_item)
|
||||||
|
|
||||||
|
|
||||||
@@ -355,6 +516,21 @@ def parse_unattend(xml_path):
|
|||||||
"SkipMachineOOBE": "true",
|
"SkipMachineOOBE": "true",
|
||||||
},
|
},
|
||||||
"firstlogon_commands": [],
|
"firstlogon_commands": [],
|
||||||
|
"user_accounts": [],
|
||||||
|
"autologon": {
|
||||||
|
"enabled": "",
|
||||||
|
"username": "",
|
||||||
|
"password": "",
|
||||||
|
"plain_text": "true",
|
||||||
|
"logon_count": "",
|
||||||
|
},
|
||||||
|
"intl": {
|
||||||
|
"input_locale": "",
|
||||||
|
"system_locale": "",
|
||||||
|
"ui_language": "",
|
||||||
|
"user_locale": "",
|
||||||
|
},
|
||||||
|
"oobe_timezone": "",
|
||||||
"raw_xml": "",
|
"raw_xml": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,6 +594,19 @@ def parse_unattend(xml_path):
|
|||||||
namespaces=ns,
|
namespaces=ns,
|
||||||
):
|
):
|
||||||
comp_name = comp.get("name", "")
|
comp_name = comp.get("name", "")
|
||||||
|
|
||||||
|
# International-Core component
|
||||||
|
if "International-Core" in comp_name:
|
||||||
|
for tag, key in [
|
||||||
|
("InputLocale", "input_locale"),
|
||||||
|
("SystemLocale", "system_locale"),
|
||||||
|
("UILanguage", "ui_language"),
|
||||||
|
("UserLocale", "user_locale"),
|
||||||
|
]:
|
||||||
|
el = comp.find(qn(tag))
|
||||||
|
if el is not None and el.text:
|
||||||
|
data["intl"][key] = el.text.strip()
|
||||||
|
|
||||||
if "OOBE" in comp_name or "Shell-Setup" in comp_name:
|
if "OOBE" in comp_name or "Shell-Setup" in comp_name:
|
||||||
oobe_el = comp.find(qn("OOBE"))
|
oobe_el = comp.find(qn("OOBE"))
|
||||||
if oobe_el is not None:
|
if oobe_el is not None:
|
||||||
@@ -426,6 +615,57 @@ def parse_unattend(xml_path):
|
|||||||
if local in data["oobe"] and child.text:
|
if local in data["oobe"] and child.text:
|
||||||
data["oobe"][local] = child.text.strip()
|
data["oobe"][local] = child.text.strip()
|
||||||
|
|
||||||
|
# UserAccounts / LocalAccounts
|
||||||
|
ua = comp.find(qn("UserAccounts"))
|
||||||
|
if ua is not None:
|
||||||
|
la_container = ua.find(qn("LocalAccounts"))
|
||||||
|
if la_container is not None:
|
||||||
|
for acct in la_container.findall(qn("LocalAccount")):
|
||||||
|
name_el = acct.find(qn("Name"))
|
||||||
|
group_el = acct.find(qn("Group"))
|
||||||
|
display_el = acct.find(qn("DisplayName"))
|
||||||
|
pw_el = acct.find(qn("Password"))
|
||||||
|
pw_val = ""
|
||||||
|
pw_plain = "true"
|
||||||
|
if pw_el is not None:
|
||||||
|
v = pw_el.find(qn("Value"))
|
||||||
|
p = pw_el.find(qn("PlainText"))
|
||||||
|
if v is not None and v.text:
|
||||||
|
pw_val = v.text.strip()
|
||||||
|
if p is not None and p.text:
|
||||||
|
pw_plain = p.text.strip()
|
||||||
|
data["user_accounts"].append({
|
||||||
|
"name": name_el.text.strip() if name_el is not None and name_el.text else "",
|
||||||
|
"password": pw_val,
|
||||||
|
"plain_text": pw_plain,
|
||||||
|
"group": group_el.text.strip() if group_el is not None and group_el.text else "Administrators",
|
||||||
|
"display_name": display_el.text.strip() if display_el is not None and display_el.text else "",
|
||||||
|
})
|
||||||
|
|
||||||
|
# AutoLogon
|
||||||
|
al = comp.find(qn("AutoLogon"))
|
||||||
|
if al is not None:
|
||||||
|
enabled_el = al.find(qn("Enabled"))
|
||||||
|
user_el = al.find(qn("Username"))
|
||||||
|
count_el = al.find(qn("LogonCount"))
|
||||||
|
pw_el = al.find(qn("Password"))
|
||||||
|
pw_val = ""
|
||||||
|
pw_plain = "true"
|
||||||
|
if pw_el is not None:
|
||||||
|
v = pw_el.find(qn("Value"))
|
||||||
|
p = pw_el.find(qn("PlainText"))
|
||||||
|
if v is not None and v.text:
|
||||||
|
pw_val = v.text.strip()
|
||||||
|
if p is not None and p.text:
|
||||||
|
pw_plain = p.text.strip()
|
||||||
|
data["autologon"] = {
|
||||||
|
"enabled": enabled_el.text.strip() if enabled_el is not None and enabled_el.text else "",
|
||||||
|
"username": user_el.text.strip() if user_el is not None and user_el.text else "",
|
||||||
|
"password": pw_val,
|
||||||
|
"plain_text": pw_plain,
|
||||||
|
"logon_count": count_el.text.strip() if count_el is not None and count_el.text else "",
|
||||||
|
}
|
||||||
|
|
||||||
# FirstLogonCommands
|
# FirstLogonCommands
|
||||||
flc = comp.find(qn("FirstLogonCommands"))
|
flc = comp.find(qn("FirstLogonCommands"))
|
||||||
if flc is not None:
|
if flc is not None:
|
||||||
@@ -439,6 +679,11 @@ def parse_unattend(xml_path):
|
|||||||
"description": desc_el.text.strip() if desc_el is not None and desc_el.text else "",
|
"description": desc_el.text.strip() if desc_el is not None and desc_el.text else "",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# TimeZone (oobeSystem pass)
|
||||||
|
tz_el = comp.find(qn("TimeZone"))
|
||||||
|
if tz_el is not None and tz_el.text:
|
||||||
|
data["oobe_timezone"] = tz_el.text.strip()
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -515,6 +760,28 @@ def build_unattend_xml(form_data):
|
|||||||
|
|
||||||
# --- oobeSystem ---
|
# --- oobeSystem ---
|
||||||
oobe_settings = _settings_pass(root, "oobeSystem")
|
oobe_settings = _settings_pass(root, "oobeSystem")
|
||||||
|
|
||||||
|
# International-Core component (before Shell-Setup)
|
||||||
|
intl = form_data.get("intl", {})
|
||||||
|
if any(v.strip() for v in intl.values() if v):
|
||||||
|
intl_comp = etree.SubElement(oobe_settings, qn("component"))
|
||||||
|
intl_comp.set("name", "Microsoft-Windows-International-Core")
|
||||||
|
intl_comp.set("processorArchitecture", "amd64")
|
||||||
|
intl_comp.set("publicKeyToken", "31bf3856ad364e35")
|
||||||
|
intl_comp.set("language", "neutral")
|
||||||
|
intl_comp.set("versionScope", "nonSxS")
|
||||||
|
for tag, key in [
|
||||||
|
("InputLocale", "input_locale"),
|
||||||
|
("SystemLocale", "system_locale"),
|
||||||
|
("UILanguage", "ui_language"),
|
||||||
|
("UserLocale", "user_locale"),
|
||||||
|
]:
|
||||||
|
val = intl.get(key, "").strip()
|
||||||
|
if val:
|
||||||
|
el = etree.SubElement(intl_comp, qn(tag))
|
||||||
|
el.text = val
|
||||||
|
|
||||||
|
# Shell-Setup component
|
||||||
oobe_comp = etree.SubElement(oobe_settings, qn("component"))
|
oobe_comp = etree.SubElement(oobe_settings, qn("component"))
|
||||||
oobe_comp.set("name", "Microsoft-Windows-Shell-Setup")
|
oobe_comp.set("name", "Microsoft-Windows-Shell-Setup")
|
||||||
oobe_comp.set("processorArchitecture", "amd64")
|
oobe_comp.set("processorArchitecture", "amd64")
|
||||||
@@ -540,6 +807,46 @@ def build_unattend_xml(form_data):
|
|||||||
child = etree.SubElement(oobe_el, qn(key))
|
child = etree.SubElement(oobe_el, qn(key))
|
||||||
child.text = str(val)
|
child.text = str(val)
|
||||||
|
|
||||||
|
# UserAccounts / LocalAccounts
|
||||||
|
accounts = form_data.get("user_accounts", [])
|
||||||
|
if accounts:
|
||||||
|
ua = etree.SubElement(oobe_comp, qn("UserAccounts"))
|
||||||
|
la_container = etree.SubElement(ua, qn("LocalAccounts"))
|
||||||
|
for acct in accounts:
|
||||||
|
if not acct.get("name", "").strip():
|
||||||
|
continue
|
||||||
|
la = etree.SubElement(la_container, qn("LocalAccount"))
|
||||||
|
la.set(qwcm("action"), "add")
|
||||||
|
pw = etree.SubElement(la, qn("Password"))
|
||||||
|
pw_val = etree.SubElement(pw, qn("Value"))
|
||||||
|
pw_val.text = acct.get("password", "")
|
||||||
|
pw_plain = etree.SubElement(pw, qn("PlainText"))
|
||||||
|
pw_plain.text = acct.get("plain_text", "true")
|
||||||
|
name_el = etree.SubElement(la, qn("Name"))
|
||||||
|
name_el.text = acct["name"].strip()
|
||||||
|
group_el = etree.SubElement(la, qn("Group"))
|
||||||
|
group_el.text = acct.get("group", "Administrators").strip()
|
||||||
|
display_el = etree.SubElement(la, qn("DisplayName"))
|
||||||
|
display_el.text = acct.get("display_name", acct["name"]).strip()
|
||||||
|
|
||||||
|
# AutoLogon
|
||||||
|
autologon = form_data.get("autologon", {})
|
||||||
|
if autologon.get("username", "").strip():
|
||||||
|
al = etree.SubElement(oobe_comp, qn("AutoLogon"))
|
||||||
|
al_pw = etree.SubElement(al, qn("Password"))
|
||||||
|
al_pw_val = etree.SubElement(al_pw, qn("Value"))
|
||||||
|
al_pw_val.text = autologon.get("password", "")
|
||||||
|
al_pw_plain = etree.SubElement(al_pw, qn("PlainText"))
|
||||||
|
al_pw_plain.text = autologon.get("plain_text", "true")
|
||||||
|
al_enabled = etree.SubElement(al, qn("Enabled"))
|
||||||
|
al_enabled.text = autologon.get("enabled", "true")
|
||||||
|
al_user = etree.SubElement(al, qn("Username"))
|
||||||
|
al_user.text = autologon["username"].strip()
|
||||||
|
logon_count = autologon.get("logon_count", "").strip()
|
||||||
|
if logon_count:
|
||||||
|
al_count = etree.SubElement(al, qn("LogonCount"))
|
||||||
|
al_count.text = logon_count
|
||||||
|
|
||||||
# FirstLogonCommands
|
# FirstLogonCommands
|
||||||
fl_cmds = form_data.get("firstlogon_commands", [])
|
fl_cmds = form_data.get("firstlogon_commands", [])
|
||||||
if fl_cmds:
|
if fl_cmds:
|
||||||
@@ -556,6 +863,12 @@ def build_unattend_xml(form_data):
|
|||||||
desc_el = etree.SubElement(sc, qn("Description"))
|
desc_el = etree.SubElement(sc, qn("Description"))
|
||||||
desc_el.text = cmd.get("description", "").strip()
|
desc_el.text = cmd.get("description", "").strip()
|
||||||
|
|
||||||
|
# TimeZone (oobeSystem pass)
|
||||||
|
oobe_tz = form_data.get("oobe_timezone", "").strip()
|
||||||
|
if oobe_tz:
|
||||||
|
tz_el = etree.SubElement(oobe_comp, qn("TimeZone"))
|
||||||
|
tz_el.text = oobe_tz
|
||||||
|
|
||||||
xml_bytes = etree.tostring(
|
xml_bytes = etree.tostring(
|
||||||
root,
|
root,
|
||||||
pretty_print=True,
|
pretty_print=True,
|
||||||
@@ -616,6 +929,40 @@ def _extract_form_data(form):
|
|||||||
"description": fl_descs[i] if i < len(fl_descs) else "",
|
"description": fl_descs[i] if i < len(fl_descs) else "",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# User accounts
|
||||||
|
accounts = []
|
||||||
|
i = 0
|
||||||
|
while form.get(f"account_name_{i}"):
|
||||||
|
accounts.append({
|
||||||
|
"name": form.get(f"account_name_{i}", ""),
|
||||||
|
"password": form.get(f"account_password_{i}", ""),
|
||||||
|
"plain_text": form.get(f"account_plaintext_{i}", "true"),
|
||||||
|
"group": form.get(f"account_group_{i}", "Administrators"),
|
||||||
|
"display_name": form.get(f"account_display_{i}", ""),
|
||||||
|
})
|
||||||
|
i += 1
|
||||||
|
data["user_accounts"] = accounts
|
||||||
|
|
||||||
|
# AutoLogon
|
||||||
|
data["autologon"] = {
|
||||||
|
"enabled": form.get("autologon_enabled", ""),
|
||||||
|
"username": form.get("autologon_username", ""),
|
||||||
|
"password": form.get("autologon_password", ""),
|
||||||
|
"plain_text": form.get("autologon_plaintext", "true"),
|
||||||
|
"logon_count": form.get("autologon_logoncount", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
# International settings
|
||||||
|
data["intl"] = {
|
||||||
|
"input_locale": form.get("intl_input_locale", ""),
|
||||||
|
"system_locale": form.get("intl_system_locale", ""),
|
||||||
|
"ui_language": form.get("intl_ui_language", ""),
|
||||||
|
"user_locale": form.get("intl_user_locale", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
# OOBE TimeZone
|
||||||
|
data["oobe_timezone"] = form.get("oobe_timezone", "")
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -799,6 +1146,74 @@ def unattend_editor(image_type):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/images/<image_type>/config")
|
||||||
|
def image_config(image_type):
|
||||||
|
if image_type not in IMAGE_TYPES:
|
||||||
|
flash("Unknown image type.", "danger")
|
||||||
|
return redirect(url_for("dashboard"))
|
||||||
|
|
||||||
|
config = _load_image_config(image_type)
|
||||||
|
return render_template(
|
||||||
|
"image_config.html",
|
||||||
|
image_type=image_type,
|
||||||
|
friendly_name=FRIENDLY_NAMES.get(image_type, image_type),
|
||||||
|
config=config,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/images/<image_type>/config/save", methods=["POST"])
|
||||||
|
def image_config_save(image_type):
|
||||||
|
if image_type not in IMAGE_TYPES:
|
||||||
|
flash("Unknown image type.", "danger")
|
||||||
|
return redirect(url_for("dashboard"))
|
||||||
|
|
||||||
|
section = request.form.get("section", "")
|
||||||
|
payload = request.form.get("payload", "[]")
|
||||||
|
try:
|
||||||
|
data = json.loads(payload)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
flash("Invalid JSON payload.", "danger")
|
||||||
|
return redirect(url_for("image_config", image_type=image_type))
|
||||||
|
|
||||||
|
ctrl = control_path(image_type)
|
||||||
|
tools = tools_path(image_type)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if section == "hardware_models":
|
||||||
|
us_file = os.path.join(tools, "user_selections.json")
|
||||||
|
us_raw = _load_json(us_file)
|
||||||
|
us_data = us_raw[0] if us_raw and isinstance(us_raw, list) else {}
|
||||||
|
us_data["HardwareModelSelection"] = data
|
||||||
|
_save_json(us_file, [us_data])
|
||||||
|
audit("CONFIG_SAVE", f"{image_type}/hardware_models")
|
||||||
|
|
||||||
|
elif section == "drivers":
|
||||||
|
# Strip internal fields before saving
|
||||||
|
clean = [{k: v for k, v in d.items() if not k.startswith("_")} for d in data]
|
||||||
|
_save_json(os.path.join(ctrl, "HardwareDriver.json"), clean)
|
||||||
|
audit("CONFIG_SAVE", f"{image_type}/drivers")
|
||||||
|
|
||||||
|
elif section == "operating_systems":
|
||||||
|
clean = [{k: v for k, v in d.items() if not k.startswith("_")} for d in data]
|
||||||
|
_save_json(os.path.join(ctrl, "OperatingSystem.json"), clean)
|
||||||
|
audit("CONFIG_SAVE", f"{image_type}/operating_systems")
|
||||||
|
|
||||||
|
elif section == "packages":
|
||||||
|
clean = [{k: v for k, v in d.items() if not k.startswith("_")} for d in data]
|
||||||
|
_save_json(os.path.join(ctrl, "packages.json"), clean)
|
||||||
|
audit("CONFIG_SAVE", f"{image_type}/packages")
|
||||||
|
|
||||||
|
else:
|
||||||
|
flash(f"Unknown section: {section}", "danger")
|
||||||
|
return redirect(url_for("image_config", image_type=image_type))
|
||||||
|
|
||||||
|
flash(f"Saved {section.replace('_', ' ')} successfully.", "success")
|
||||||
|
except Exception as exc:
|
||||||
|
flash(f"Failed to save {section}: {exc}", "danger")
|
||||||
|
|
||||||
|
return redirect(url_for("image_config", image_type=image_type))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Routes — Clonezilla Backups
|
# Routes — Clonezilla Backups
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -922,6 +1337,78 @@ def blancco_delete_report(filename):
|
|||||||
return redirect(url_for("blancco_reports"))
|
return redirect(url_for("blancco_reports"))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Routes — Enrollment Packages
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@app.route("/enrollment")
|
||||||
|
def enrollment():
|
||||||
|
packages = []
|
||||||
|
if os.path.isdir(ENROLLMENT_SHARE):
|
||||||
|
for f in sorted(os.listdir(ENROLLMENT_SHARE)):
|
||||||
|
fpath = os.path.join(ENROLLMENT_SHARE, f)
|
||||||
|
if os.path.isfile(fpath) and f.lower().endswith(".ppkg"):
|
||||||
|
stat = os.stat(fpath)
|
||||||
|
packages.append({
|
||||||
|
"filename": f,
|
||||||
|
"size": stat.st_size,
|
||||||
|
"modified": stat.st_mtime,
|
||||||
|
})
|
||||||
|
return render_template(
|
||||||
|
"enrollment.html",
|
||||||
|
packages=packages,
|
||||||
|
image_types=IMAGE_TYPES,
|
||||||
|
friendly_names=FRIENDLY_NAMES,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/enrollment/upload", methods=["POST"])
|
||||||
|
def enrollment_upload():
|
||||||
|
if "ppkg_file" not in request.files:
|
||||||
|
flash("No file selected.", "danger")
|
||||||
|
return redirect(url_for("enrollment"))
|
||||||
|
|
||||||
|
f = request.files["ppkg_file"]
|
||||||
|
if not f.filename:
|
||||||
|
flash("No file selected.", "danger")
|
||||||
|
return redirect(url_for("enrollment"))
|
||||||
|
|
||||||
|
filename = secure_filename(f.filename)
|
||||||
|
if not filename.lower().endswith(".ppkg"):
|
||||||
|
flash("Only .ppkg files are accepted.", "danger")
|
||||||
|
return redirect(url_for("enrollment"))
|
||||||
|
|
||||||
|
os.makedirs(ENROLLMENT_SHARE, exist_ok=True)
|
||||||
|
dest = os.path.join(ENROLLMENT_SHARE, filename)
|
||||||
|
f.save(dest)
|
||||||
|
audit("ENROLLMENT_UPLOAD", filename)
|
||||||
|
flash(f"Uploaded {filename} successfully.", "success")
|
||||||
|
return redirect(url_for("enrollment"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/enrollment/download/<filename>")
|
||||||
|
def enrollment_download(filename):
|
||||||
|
filename = secure_filename(filename)
|
||||||
|
fpath = os.path.join(ENROLLMENT_SHARE, filename)
|
||||||
|
if not os.path.isfile(fpath):
|
||||||
|
flash(f"Package not found: {filename}", "danger")
|
||||||
|
return redirect(url_for("enrollment"))
|
||||||
|
return send_file(fpath, as_attachment=True)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/enrollment/delete/<filename>", methods=["POST"])
|
||||||
|
def enrollment_delete(filename):
|
||||||
|
filename = secure_filename(filename)
|
||||||
|
fpath = os.path.join(ENROLLMENT_SHARE, filename)
|
||||||
|
if os.path.isfile(fpath):
|
||||||
|
os.remove(fpath)
|
||||||
|
audit("ENROLLMENT_DELETE", filename)
|
||||||
|
flash(f"Deleted {filename}.", "success")
|
||||||
|
else:
|
||||||
|
flash(f"Package not found: {filename}", "danger")
|
||||||
|
return redirect(url_for("enrollment"))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Routes — startnet.cmd Editor (WIM)
|
# Routes — startnet.cmd Editor (WIM)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -150,6 +150,71 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Add User Account
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
var addUserAccountBtn = document.getElementById('addUserAccount');
|
||||||
|
if (addUserAccountBtn) {
|
||||||
|
addUserAccountBtn.addEventListener('click', function () {
|
||||||
|
var tbody = document.querySelector('#userAccountsTable tbody');
|
||||||
|
var idx = tbody.querySelectorAll('tr').length;
|
||||||
|
var tr = document.createElement('tr');
|
||||||
|
tr.innerHTML =
|
||||||
|
'<td class="order-num">' + (idx + 1) + '</td>' +
|
||||||
|
'<td><input type="text" class="form-control form-control-sm" name="account_name_' + idx + '" value="" placeholder="Username"></td>' +
|
||||||
|
'<td><input type="text" class="form-control form-control-sm" name="account_password_' + idx + '" value="" placeholder="Password"></td>' +
|
||||||
|
'<td><input type="text" class="form-control form-control-sm" name="account_group_' + idx + '" value="Administrators"></td>' +
|
||||||
|
'<td><input type="text" class="form-control form-control-sm" name="account_display_' + idx + '" value="" placeholder="Display Name"></td>' +
|
||||||
|
'<td>' +
|
||||||
|
'<input type="hidden" name="account_plaintext_' + idx + '" value="true">' +
|
||||||
|
'<button type="button" class="btn btn-outline-danger btn-row-action remove-account-row">Remove</button>' +
|
||||||
|
'</td>';
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
var emptyEl = document.getElementById('userAccountsEmpty');
|
||||||
|
if (emptyEl) emptyEl.style.display = 'none';
|
||||||
|
tr.querySelector('input[name^="account_name_"]').focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Remove User Account row + renumber indices (delegated)
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
var btn = e.target.closest('.remove-account-row');
|
||||||
|
if (!btn) return;
|
||||||
|
var row = btn.closest('tr');
|
||||||
|
var tbody = row.parentElement;
|
||||||
|
row.remove();
|
||||||
|
// Renumber order and field name indices
|
||||||
|
var rows = tbody.querySelectorAll('tr');
|
||||||
|
rows.forEach(function (r, i) {
|
||||||
|
var orderCell = r.querySelector('.order-num');
|
||||||
|
if (orderCell) orderCell.textContent = i + 1;
|
||||||
|
// Rename inputs to match new index
|
||||||
|
r.querySelectorAll('input').forEach(function (inp) {
|
||||||
|
var name = inp.getAttribute('name');
|
||||||
|
if (name) {
|
||||||
|
inp.setAttribute('name', name.replace(/_\d+$/, '_' + i));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var emptyEl = document.getElementById('userAccountsEmpty');
|
||||||
|
if (emptyEl) emptyEl.style.display = rows.length > 0 ? 'none' : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// AutoLogon Enabled toggle — keep hidden input in sync
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
var autologonToggle = document.getElementById('autologonEnabledToggle');
|
||||||
|
if (autologonToggle) {
|
||||||
|
autologonToggle.addEventListener('change', function () {
|
||||||
|
var hidden = document.getElementById('autologon_enabled_val');
|
||||||
|
if (hidden) {
|
||||||
|
hidden.value = this.checked ? 'true' : 'false';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// OOBE toggle switches — keep hidden input in sync
|
// OOBE toggle switches — keep hidden input in sync
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
@@ -298,5 +363,107 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
toggleEmpty('driverPathsTable', 'driverPathsEmpty');
|
toggleEmpty('driverPathsTable', 'driverPathsEmpty');
|
||||||
toggleEmpty('specCmdTable', 'specCmdEmpty');
|
toggleEmpty('specCmdTable', 'specCmdEmpty');
|
||||||
toggleEmpty('flCmdTable', 'flCmdEmpty');
|
toggleEmpty('flCmdTable', 'flCmdEmpty');
|
||||||
|
toggleEmpty('userAccountsTable', 'userAccountsEmpty');
|
||||||
|
|
||||||
|
// =======================================================================
|
||||||
|
// Image Configuration Editor handlers
|
||||||
|
// =======================================================================
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Generic: collect table rows into JSON and submit form
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
function collectAndSubmit(tableId, hiddenId, formId, extractor) {
|
||||||
|
var tbody = document.querySelector('#' + tableId + ' tbody');
|
||||||
|
if (!tbody) return;
|
||||||
|
var rows = tbody.querySelectorAll('tr');
|
||||||
|
var data = [];
|
||||||
|
rows.forEach(function (row) {
|
||||||
|
var item = extractor(row);
|
||||||
|
if (item) data.push(item);
|
||||||
|
});
|
||||||
|
document.getElementById(hiddenId).value = JSON.stringify(data);
|
||||||
|
document.getElementById(formId).submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: extract JSON from data-json attr, stripping internal _fields
|
||||||
|
function extractDataJson(row) {
|
||||||
|
var raw = row.getAttribute('data-json');
|
||||||
|
if (!raw) return null;
|
||||||
|
try {
|
||||||
|
var obj = JSON.parse(raw);
|
||||||
|
Object.keys(obj).forEach(function (k) {
|
||||||
|
if (k.charAt(0) === '_') delete obj[k];
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
} catch (e) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Save: Hardware Models
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
var saveHwModelsBtn = document.getElementById('saveHwModels');
|
||||||
|
if (saveHwModelsBtn) {
|
||||||
|
saveHwModelsBtn.addEventListener('click', function () {
|
||||||
|
collectAndSubmit('hwModelsTable', 'hwModelsData', 'hwModelsForm', function (row) {
|
||||||
|
var m = row.querySelector('[data-field="Model"]');
|
||||||
|
var f = row.querySelector('[data-field="Id"]');
|
||||||
|
if (!m || !f) return null;
|
||||||
|
return { Model: m.value, Id: f.value };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Save: Drivers
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
var saveDriversBtn = document.getElementById('saveDrivers');
|
||||||
|
if (saveDriversBtn) {
|
||||||
|
saveDriversBtn.addEventListener('click', function () {
|
||||||
|
collectAndSubmit('driversTable', 'driversData', 'driversForm', extractDataJson);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Save: Operating Systems
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
var saveOsBtn = document.getElementById('saveOs');
|
||||||
|
if (saveOsBtn) {
|
||||||
|
saveOsBtn.addEventListener('click', function () {
|
||||||
|
collectAndSubmit('osTable', 'osData', 'osForm', extractDataJson);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Save: Packages
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
var savePackagesBtn = document.getElementById('savePackages');
|
||||||
|
if (savePackagesBtn) {
|
||||||
|
savePackagesBtn.addEventListener('click', function () {
|
||||||
|
collectAndSubmit('packagesTable', 'packagesData', 'packagesForm', extractDataJson);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Add Hardware Model row
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
var addHwModelBtn = document.getElementById('addHwModel');
|
||||||
|
if (addHwModelBtn) {
|
||||||
|
addHwModelBtn.addEventListener('click', function () {
|
||||||
|
var tbody = document.querySelector('#hwModelsTable tbody');
|
||||||
|
var idx = tbody.querySelectorAll('tr').length + 1;
|
||||||
|
var tr = document.createElement('tr');
|
||||||
|
tr.innerHTML =
|
||||||
|
'<td class="order-num">' + idx + '</td>' +
|
||||||
|
'<td><input type="text" class="form-control form-control-sm" data-field="Model" value="" placeholder="Model name"></td>' +
|
||||||
|
'<td><input type="text" class="form-control form-control-sm" data-field="Id" value="" placeholder="Driver Family ID"></td>' +
|
||||||
|
'<td><span class="badge bg-secondary badge-disk">New</span></td>' +
|
||||||
|
'<td><button type="button" class="btn btn-outline-danger btn-row-action remove-row"><i class="bi bi-trash"></i></button></td>';
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
var empty = document.getElementById('hwModelsEmpty');
|
||||||
|
if (empty) empty.style.display = 'none';
|
||||||
|
tr.querySelector('input').focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h2 class="mb-0"><i class="bi bi-journal-text me-2"></i>Audit Log</h2>
|
<h2 class="mb-0">Audit Log</h2>
|
||||||
<span class="badge bg-secondary fs-6">{{ entries|length }} entries</span>
|
<span class="badge bg-secondary fs-6">{{ entries|length }} entries</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header d-flex align-items-center">
|
<div class="card-header d-flex align-items-center">
|
||||||
<i class="bi bi-clock-history me-2"></i> Activity History
|
Activity History
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
{% if entries %}
|
{% if entries %}
|
||||||
@@ -52,7 +52,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center text-muted py-5">
|
<div class="text-center text-muted py-5">
|
||||||
<i class="bi bi-journal-text" style="font-size: 3rem;"></i>
|
|
||||||
<p class="mt-2">No audit log entries yet.</p>
|
<p class="mt-2">No audit log entries yet.</p>
|
||||||
<p class="small">Actions like image imports, unattend edits, and backup operations will be logged here.</p>
|
<p class="small">Actions like image imports, unattend edits, and backup operations will be logged here.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,15 +3,15 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h2 class="mb-0"><i class="bi bi-archive me-2"></i>Clonezilla Backups</h2>
|
<h2 class="mb-0">Clonezilla Backups</h2>
|
||||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadModal">
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadModal">
|
||||||
<i class="bi bi-upload me-1"></i> Upload Backup
|
Upload Backup
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header d-flex align-items-center">
|
<div class="card-header d-flex align-items-center">
|
||||||
<i class="bi bi-hdd-stack me-2"></i> Machine Backups
|
Machine Backups
|
||||||
<span class="badge bg-secondary ms-2">{{ backups|length }}</span>
|
<span class="badge bg-secondary ms-2">{{ backups|length }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
@@ -36,12 +36,12 @@
|
|||||||
<td class="text-end text-nowrap">
|
<td class="text-end text-nowrap">
|
||||||
<a href="{{ url_for('clonezilla_download', filename=b.filename) }}"
|
<a href="{{ url_for('clonezilla_download', filename=b.filename) }}"
|
||||||
class="btn btn-sm btn-outline-primary" title="Download">
|
class="btn btn-sm btn-outline-primary" title="Download">
|
||||||
<i class="bi bi-download"></i>
|
Download
|
||||||
</a>
|
</a>
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger"
|
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||||
data-filename="{{ b.filename }}" data-machine="{{ b.machine }}" title="Delete">
|
data-filename="{{ b.filename }}" data-machine="{{ b.machine }}" title="Delete">
|
||||||
<i class="bi bi-trash"></i>
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -50,7 +50,6 @@
|
|||||||
</table>
|
</table>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center text-muted py-5">
|
<div class="text-center text-muted py-5">
|
||||||
<i class="bi bi-archive" style="font-size: 3rem;"></i>
|
|
||||||
<p class="mt-2">No backups found. Upload a Clonezilla backup .zip to get started.</p>
|
<p class="mt-2">No backups found. Upload a Clonezilla backup .zip to get started.</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -59,7 +58,7 @@
|
|||||||
|
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h6 class="card-title"><i class="bi bi-info-circle me-1"></i> Backup Naming Convention</h6>
|
<h6 class="card-title">Backup Naming Convention</h6>
|
||||||
<p class="card-text mb-0">
|
<p class="card-text mb-0">
|
||||||
Name backup files with the machine number (e.g., <code>6501.zip</code>).
|
Name backup files with the machine number (e.g., <code>6501.zip</code>).
|
||||||
The Samba share <code>\\pxe-server\clonezilla</code> is also available on the network for direct Clonezilla save/restore operations.
|
The Samba share <code>\\pxe-server\clonezilla</code> is also available on the network for direct Clonezilla save/restore operations.
|
||||||
@@ -74,7 +73,7 @@
|
|||||||
<form action="{{ url_for('clonezilla_upload') }}" method="post" enctype="multipart/form-data">
|
<form action="{{ url_for('clonezilla_upload') }}" method="post" enctype="multipart/form-data">
|
||||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title"><i class="bi bi-upload me-2"></i>Upload Backup</h5>
|
<h5 class="modal-title">Upload Backup</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@@ -89,7 +88,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
<button type="submit" class="btn btn-primary"><i class="bi bi-upload me-1"></i> Upload</button>
|
<button type="submit" class="btn btn-primary">Upload</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +102,7 @@
|
|||||||
<form id="deleteForm" method="post">
|
<form id="deleteForm" method="post">
|
||||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title"><i class="bi bi-exclamation-triangle me-2 text-danger"></i>Confirm Delete</h5>
|
<h5 class="modal-title">Confirm Delete</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@@ -112,7 +111,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
<button type="submit" class="btn btn-danger"><i class="bi bi-trash me-1"></i> Delete</button>
|
<button type="submit" class="btn btn-danger">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
<title>{% block title %}PXE Server Manager{% endblock %}</title>
|
<title>{% block title %}PXE Server Manager{% endblock %}</title>
|
||||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
|
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
|
||||||
<link href="{{ url_for('static', filename='bootstrap.min.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='bootstrap.min.css') }}" rel="stylesheet">
|
||||||
<link href="{{ url_for('static', filename='bootstrap-icons.min.css') }}" rel="stylesheet">
|
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--sidebar-width: 280px;
|
--sidebar-width: 280px;
|
||||||
@@ -36,10 +35,6 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: rgba(255,255,255,0.08);
|
background-color: rgba(255,255,255,0.08);
|
||||||
}
|
}
|
||||||
.sidebar .nav-link .bi {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
.sidebar-heading {
|
.sidebar-heading {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -59,10 +54,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.3rem;
|
gap: 0.3rem;
|
||||||
}
|
}
|
||||||
.sidebar .brand .bi {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
color: #0d6efd;
|
|
||||||
}
|
|
||||||
.main-content {
|
.main-content {
|
||||||
margin-left: var(--sidebar-width);
|
margin-left: var(--sidebar-width);
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
@@ -121,13 +112,13 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.endpoint == 'dashboard' %}active{% endif %}"
|
<a class="nav-link {% if request.endpoint == 'dashboard' %}active{% endif %}"
|
||||||
href="{{ url_for('dashboard') }}">
|
href="{{ url_for('dashboard') }}">
|
||||||
<i class="bi bi-speedometer2"></i> Dashboard
|
Dashboard
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.endpoint == 'images_import' %}active{% endif %}"
|
<a class="nav-link {% if request.endpoint == 'images_import' %}active{% endif %}"
|
||||||
href="{{ url_for('images_import') }}">
|
href="{{ url_for('images_import') }}">
|
||||||
<i class="bi bi-download"></i> Image Import
|
Image Import
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -138,25 +129,31 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.endpoint == 'startnet_editor' %}active{% endif %}"
|
<a class="nav-link {% if request.endpoint == 'startnet_editor' %}active{% endif %}"
|
||||||
href="{{ url_for('startnet_editor') }}">
|
href="{{ url_for('startnet_editor') }}">
|
||||||
<i class="bi bi-terminal"></i> startnet.cmd
|
startnet.cmd
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.endpoint == 'clonezilla_backups' %}active{% endif %}"
|
<a class="nav-link {% if request.endpoint == 'clonezilla_backups' %}active{% endif %}"
|
||||||
href="{{ url_for('clonezilla_backups') }}">
|
href="{{ url_for('clonezilla_backups') }}">
|
||||||
<i class="bi bi-archive"></i> Clonezilla Backups
|
Clonezilla Backups
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.endpoint == 'blancco_reports' %}active{% endif %}"
|
<a class="nav-link {% if request.endpoint == 'blancco_reports' %}active{% endif %}"
|
||||||
href="{{ url_for('blancco_reports') }}">
|
href="{{ url_for('blancco_reports') }}">
|
||||||
<i class="bi bi-shield-check"></i> Blancco Reports
|
Blancco Reports
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.endpoint == 'enrollment' %}active{% endif %}"
|
||||||
|
href="{{ url_for('enrollment') }}">
|
||||||
|
Enrollment
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.endpoint == 'audit_log' %}active{% endif %}"
|
<a class="nav-link {% if request.endpoint == 'audit_log' %}active{% endif %}"
|
||||||
href="{{ url_for('audit_log') }}">
|
href="{{ url_for('audit_log') }}">
|
||||||
<i class="bi bi-journal-text"></i> Audit Log
|
Audit Log
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -168,7 +165,13 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.endpoint == 'unattend_editor' and image_type is defined and image_type == it %}active{% endif %}"
|
<a class="nav-link {% if request.endpoint == 'unattend_editor' and image_type is defined and image_type == it %}active{% endif %}"
|
||||||
href="{{ url_for('unattend_editor', image_type=it) }}">
|
href="{{ url_for('unattend_editor', image_type=it) }}">
|
||||||
<i class="bi bi-file-earmark-code"></i> {{ all_friendly_names[it] }}
|
{{ all_friendly_names[it] }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.endpoint == 'image_config' and image_type is defined and image_type == it %}active{% endif %}"
|
||||||
|
href="{{ url_for('image_config', image_type=it) }}" style="padding-left: 2.5rem; font-size: 0.82rem;">
|
||||||
|
Configuration
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -181,7 +184,13 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.endpoint == 'unattend_editor' and image_type is defined and image_type == it %}active{% endif %}"
|
<a class="nav-link {% if request.endpoint == 'unattend_editor' and image_type is defined and image_type == it %}active{% endif %}"
|
||||||
href="{{ url_for('unattend_editor', image_type=it) }}">
|
href="{{ url_for('unattend_editor', image_type=it) }}">
|
||||||
<i class="bi bi-file-earmark-code"></i> {{ all_friendly_names[it] }}
|
{{ all_friendly_names[it] }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.endpoint == 'image_config' and image_type is defined and image_type == it %}active{% endif %}"
|
||||||
|
href="{{ url_for('image_config', image_type=it) }}" style="padding-left: 2.5rem; font-size: 0.82rem;">
|
||||||
|
Configuration
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h2 class="mb-0">Dashboard</h2>
|
<h2 class="mb-0">Dashboard</h2>
|
||||||
<button class="btn btn-outline-secondary btn-sm" onclick="location.reload()">
|
<button class="btn btn-outline-secondary btn-sm" onclick="location.reload()">
|
||||||
<i class="bi bi-arrow-clockwise"></i> Refresh
|
Refresh
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Services -->
|
<!-- Services -->
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex align-items-center">
|
<div class="card-header d-flex align-items-center">
|
||||||
<i class="bi bi-gear-wide-connected me-2"></i> PXE Services
|
PXE Services
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
@@ -27,7 +27,6 @@
|
|||||||
{% for svc in services %}
|
{% for svc in services %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<i class="bi bi-server me-1 text-muted"></i>
|
|
||||||
<strong>{{ svc.name }}</strong>
|
<strong>{{ svc.name }}</strong>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -45,7 +44,7 @@
|
|||||||
<!-- Images -->
|
<!-- Images -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header d-flex align-items-center">
|
<div class="card-header d-flex align-items-center">
|
||||||
<i class="bi bi-disc me-2"></i> Deployment Images
|
Deployment Images
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
@@ -67,23 +66,27 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if img.has_content %}
|
{% if img.has_content %}
|
||||||
<span class="badge bg-success"><i class="bi bi-check-circle"></i> Present</span>
|
<span class="badge bg-success">Present</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-secondary"><i class="bi bi-x-circle"></i> Empty</span>
|
<span class="badge bg-secondary">Empty</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if img.has_unattend %}
|
{% if img.has_unattend %}
|
||||||
<span class="badge bg-success"><i class="bi bi-check-circle"></i> Exists</span>
|
<span class="badge bg-success">Exists</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-warning text-dark"><i class="bi bi-exclamation-triangle"></i> Missing</span>
|
<span class="badge bg-warning text-dark">Missing</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td><code class="small">{{ img.deploy_path }}</code></td>
|
<td><code class="small">{{ img.deploy_path }}</code></td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
|
<a href="{{ url_for('image_config', image_type=img.image_type) }}"
|
||||||
|
class="btn btn-sm btn-outline-secondary me-1">
|
||||||
|
Config
|
||||||
|
</a>
|
||||||
<a href="{{ url_for('unattend_editor', image_type=img.image_type) }}"
|
<a href="{{ url_for('unattend_editor', image_type=img.image_type) }}"
|
||||||
class="btn btn-sm btn-outline-primary">
|
class="btn btn-sm btn-outline-primary">
|
||||||
<i class="bi bi-pencil-square"></i> Edit Unattend
|
Edit Unattend
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
135
webapp/templates/enrollment.html
Normal file
135
webapp/templates/enrollment.html
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Enrollment Packages - PXE Server Manager{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="mb-0">Enrollment Packages</h2>
|
||||||
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadModal">
|
||||||
|
Upload Package
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex align-items-center">
|
||||||
|
GCCH Provisioning Packages
|
||||||
|
<span class="badge bg-secondary ms-2">{{ packages|length }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if packages %}
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Filename</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Last Modified</th>
|
||||||
|
<th class="text-end">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in packages %}
|
||||||
|
<tr>
|
||||||
|
<td><code>{{ p.filename }}</code></td>
|
||||||
|
<td>{{ "%.1f"|format(p.size / 1048576) }} MB</td>
|
||||||
|
<td>{{ p.modified | timestamp_fmt }}</td>
|
||||||
|
<td class="text-end text-nowrap">
|
||||||
|
<a href="{{ url_for('enrollment_download', filename=p.filename) }}"
|
||||||
|
class="btn btn-sm btn-outline-primary" title="Download">
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||||
|
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||||
|
data-filename="{{ p.filename }}" title="Delete">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center text-muted py-5">
|
||||||
|
<p class="mt-2">No enrollment packages found. Upload a <code>.ppkg</code> file to get started.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title">About Enrollment Packages</h6>
|
||||||
|
<p class="card-text">
|
||||||
|
GCCH enrollment <code>.ppkg</code> provisioning packages are copied to
|
||||||
|
<code>C:\Enrollment\</code> on the target machine after imaging.
|
||||||
|
At OOBE, connect to a network with internet, press <strong>Windows key 5 times</strong>,
|
||||||
|
then browse to <code>C:\Enrollment\</code> and select the package. No USB stick needed.
|
||||||
|
</p>
|
||||||
|
<p class="card-text mb-0">
|
||||||
|
<strong>Naming convention:</strong> Use <code>with-office.ppkg</code> and
|
||||||
|
<code>without-office.ppkg</code> to match the WinPE enrollment menu options.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload Modal -->
|
||||||
|
<div class="modal fade" id="uploadModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form action="{{ url_for('enrollment_upload') }}" method="post" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Upload Enrollment Package</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ppkgFile" class="form-label">Provisioning Package (.ppkg)</label>
|
||||||
|
<input type="file" class="form-control" id="ppkgFile" name="ppkg_file"
|
||||||
|
accept=".ppkg" required>
|
||||||
|
<div class="form-text">
|
||||||
|
Use <code>with-office.ppkg</code> or <code>without-office.ppkg</code> to match the WinPE boot menu.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Upload</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Modal -->
|
||||||
|
<div class="modal fade" id="deleteModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form id="deleteForm" method="post">
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Confirm Delete</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Are you sure you want to delete <strong id="deleteFilename"></strong>?</p>
|
||||||
|
<p class="text-muted mb-0">This action cannot be undone.</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-danger">Delete</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_scripts %}
|
||||||
|
<script>
|
||||||
|
document.getElementById('deleteModal').addEventListener('show.bs.modal', function (event) {
|
||||||
|
var btn = event.relatedTarget;
|
||||||
|
var filename = btn.getAttribute('data-filename');
|
||||||
|
document.getElementById('deleteFilename').textContent = filename;
|
||||||
|
document.getElementById('deleteForm').action = '/enrollment/delete/' + encodeURIComponent(filename);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
331
webapp/templates/image_config.html
Normal file
331
webapp/templates/image_config.html
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{{ friendly_name }} - Configuration{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_head %}
|
||||||
|
<style>
|
||||||
|
.section-card { margin-bottom: 1.5rem; }
|
||||||
|
.section-card .card-header { padding: 0.6rem 1rem; font-size: 0.95rem; }
|
||||||
|
.badge-disk { font-size: 0.75rem; }
|
||||||
|
.orphan-section { background-color: #fff8e1; }
|
||||||
|
.config-table td, .config-table th { vertical-align: middle; }
|
||||||
|
.config-table .form-control-sm { min-width: 120px; }
|
||||||
|
.text-truncate-cell { max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="mb-1">{{ friendly_name }}</h2>
|
||||||
|
<small class="text-muted">
|
||||||
|
Image Configuration
|
||||||
|
— OS Selection: <strong>{{ config.os_selection or 'Not set' }}</strong>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('unattend_editor', image_type=image_type) }}" class="btn btn-outline-secondary btn-sm">
|
||||||
|
Edit Unattend
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ==================== SECTION 1: Hardware Models ==================== #}
|
||||||
|
<div class="card section-card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<span>Hardware Models
|
||||||
|
<span class="badge bg-secondary ms-1">{{ config.hardware_models|length }}</span>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" id="addHwModel">
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-success ms-1" id="saveHwModels">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<form method="POST" action="{{ url_for('image_config_save', image_type=image_type) }}" id="hwModelsForm">
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<input type="hidden" name="section" value="hardware_models">
|
||||||
|
<input type="hidden" name="payload" id="hwModelsData" value="[]">
|
||||||
|
</form>
|
||||||
|
<table class="table table-sm table-hover mb-0 config-table" id="hwModelsTable">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width:40px">#</th>
|
||||||
|
<th>Model</th>
|
||||||
|
<th>Driver Family ID</th>
|
||||||
|
<th style="width:90px">On Disk</th>
|
||||||
|
<th style="width:60px"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for hm in config.hardware_models %}
|
||||||
|
<tr>
|
||||||
|
<td class="order-num">{{ loop.index }}</td>
|
||||||
|
<td><input type="text" class="form-control form-control-sm" data-field="Model" value="{{ hm.Model }}"></td>
|
||||||
|
<td><input type="text" class="form-control form-control-sm" data-field="Id" value="{{ hm.Id }}"></td>
|
||||||
|
<td>
|
||||||
|
{% if hm._on_disk %}
|
||||||
|
<span class="badge bg-success badge-disk">Yes</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger badge-disk">No</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% if not config.hardware_models %}
|
||||||
|
<div class="text-center text-muted py-3 empty-message" id="hwModelsEmpty">
|
||||||
|
No hardware models configured.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ==================== SECTION 2: Driver Packs ==================== #}
|
||||||
|
<div class="card section-card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<span>Driver Packs
|
||||||
|
<span class="badge bg-secondary ms-1">{{ config.drivers|length }}</span>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-sm btn-success" id="saveDrivers">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<form method="POST" action="{{ url_for('image_config_save', image_type=image_type) }}" id="driversForm">
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<input type="hidden" name="section" value="drivers">
|
||||||
|
<input type="hidden" name="payload" id="driversData" value="[]">
|
||||||
|
</form>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-hover mb-0 config-table" id="driversTable">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width:40px">#</th>
|
||||||
|
<th>Family</th>
|
||||||
|
<th>Models</th>
|
||||||
|
<th>File Name</th>
|
||||||
|
<th style="width:70px">OS IDs</th>
|
||||||
|
<th style="width:90px">On Disk</th>
|
||||||
|
<th style="width:60px"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for drv in config.drivers %}
|
||||||
|
<tr data-json='{{ drv | tojson }}'>
|
||||||
|
<td class="order-num">{{ loop.index }}</td>
|
||||||
|
<td class="text-truncate-cell" title="{{ drv.family }}">{{ drv.family }}</td>
|
||||||
|
<td class="text-truncate-cell" title="{{ drv.models }}">{{ drv.models }}</td>
|
||||||
|
<td class="text-truncate-cell" title="{{ drv.FileName or drv.get('fileName','') }}">
|
||||||
|
<small>{{ drv.FileName or drv.get('fileName','') }}</small>
|
||||||
|
</td>
|
||||||
|
<td><small>{{ drv.osId }}</small></td>
|
||||||
|
<td>
|
||||||
|
{% if drv._on_disk %}
|
||||||
|
<span class="badge bg-success badge-disk">Yes</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger badge-disk">No</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% if not config.drivers %}
|
||||||
|
<div class="text-center text-muted py-3 empty-message" id="driversEmpty">
|
||||||
|
No driver packs configured.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Orphan drivers sub-section #}
|
||||||
|
{% if config.orphan_drivers %}
|
||||||
|
<div class="card-footer orphan-section p-0">
|
||||||
|
<div class="px-3 py-2">
|
||||||
|
<a class="text-decoration-none" data-bs-toggle="collapse" href="#orphanDrivers" role="button">
|
||||||
|
<strong>Unregistered Drivers ({{ config.orphan_drivers|length }})</strong>
|
||||||
|
<small class="text-muted ms-1">zip files on disk not in any JSON</small>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="collapse" id="orphanDrivers">
|
||||||
|
<table class="table table-sm mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>File Name</th>
|
||||||
|
<th>Relative Path</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for orph in config.orphan_drivers %}
|
||||||
|
<tr>
|
||||||
|
<td><small>{{ orph.fileName }}</small></td>
|
||||||
|
<td><small class="text-muted">{{ orph.relPath }}</small></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ==================== SECTION 3: Operating Systems ==================== #}
|
||||||
|
<div class="card section-card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<span>Operating Systems
|
||||||
|
<span class="badge bg-secondary ms-1">{{ config.operating_systems|length }}</span>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-sm btn-success" id="saveOs">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<form method="POST" action="{{ url_for('image_config_save', image_type=image_type) }}" id="osForm">
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<input type="hidden" name="section" value="operating_systems">
|
||||||
|
<input type="hidden" name="payload" id="osData" value="[]">
|
||||||
|
</form>
|
||||||
|
<table class="table table-sm table-hover mb-0 config-table" id="osTable">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width:40px">#</th>
|
||||||
|
<th>Product Name</th>
|
||||||
|
<th>Version</th>
|
||||||
|
<th>Build</th>
|
||||||
|
<th style="width:60px">ID</th>
|
||||||
|
<th style="width:70px">Active</th>
|
||||||
|
<th style="width:90px">WIM On Disk</th>
|
||||||
|
<th style="width:60px"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for entry in config.operating_systems %}
|
||||||
|
{% set osv = entry.operatingSystemVersion %}
|
||||||
|
<tr data-json='{{ entry | tojson }}'>
|
||||||
|
<td class="order-num">{{ loop.index }}</td>
|
||||||
|
<td>{{ osv.productName }}</td>
|
||||||
|
<td>{{ osv.versionNumber }}</td>
|
||||||
|
<td>{{ osv.buildNumber }}</td>
|
||||||
|
<td>{{ osv.id }}</td>
|
||||||
|
<td>
|
||||||
|
{% if osv.isActive %}
|
||||||
|
<span class="badge bg-success badge-disk">Active</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary badge-disk">Inactive</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if entry._on_disk %}
|
||||||
|
<span class="badge bg-success badge-disk">Yes</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger badge-disk">No</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% if not config.operating_systems %}
|
||||||
|
<div class="text-center text-muted py-3 empty-message" id="osEmpty">
|
||||||
|
No operating systems configured.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ==================== SECTION 4: Packages ==================== #}
|
||||||
|
<div class="card section-card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<span>Packages
|
||||||
|
<span class="badge bg-secondary ms-1">{{ config.packages|length }}</span>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-sm btn-success" id="savePackages">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<form method="POST" action="{{ url_for('image_config_save', image_type=image_type) }}" id="packagesForm">
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<input type="hidden" name="section" value="packages">
|
||||||
|
<input type="hidden" name="payload" id="packagesData" value="[]">
|
||||||
|
</form>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-hover mb-0 config-table" id="packagesTable">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width:40px">#</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Comment</th>
|
||||||
|
<th>File</th>
|
||||||
|
<th style="width:70px">OS IDs</th>
|
||||||
|
<th style="width:80px">Enabled</th>
|
||||||
|
<th style="width:90px">On Disk</th>
|
||||||
|
<th style="width:60px"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for pkg in config.packages %}
|
||||||
|
<tr data-json='{{ pkg | tojson }}'>
|
||||||
|
<td class="order-num">{{ loop.index }}</td>
|
||||||
|
<td class="text-truncate-cell" title="{{ pkg.name }}"><small>{{ pkg.name }}</small></td>
|
||||||
|
<td class="text-truncate-cell" title="{{ pkg.comment }}"><small>{{ pkg.comment }}</small></td>
|
||||||
|
<td class="text-truncate-cell" title="{{ pkg.fileName or pkg.get('FileName','') }}">
|
||||||
|
<small>{{ pkg.fileName or pkg.get('FileName','') }}</small>
|
||||||
|
</td>
|
||||||
|
<td><small>{{ pkg.osId }}</small></td>
|
||||||
|
<td>
|
||||||
|
{% if pkg.enabled %}
|
||||||
|
<span class="badge bg-success badge-disk">Yes</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary badge-disk">No</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if pkg._on_disk %}
|
||||||
|
<span class="badge bg-success badge-disk">Yes</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger badge-disk">No</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% if not config.packages %}
|
||||||
|
<div class="text-center text-muted py-3 empty-message" id="packagesEmpty">
|
||||||
|
No packages configured.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<!-- Network Upload Import -->
|
<!-- Network Upload Import -->
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<i class="bi bi-cloud-upload me-2"></i> Import from Network Upload
|
Import from Network Upload
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if upload_sources %}
|
{% if upload_sources %}
|
||||||
@@ -44,7 +44,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-info d-flex align-items-start" role="alert">
|
<div class="alert alert-info d-flex align-items-start" role="alert">
|
||||||
<i class="bi bi-info-circle-fill me-2 mt-1"></i>
|
|
||||||
<div>
|
<div>
|
||||||
<strong>Shared Drivers:</strong> Out-of-box Drivers are automatically pooled
|
<strong>Shared Drivers:</strong> Out-of-box Drivers are automatically pooled
|
||||||
into a shared directory and symlinked for each image type to save disk space.
|
into a shared directory and symlinked for each image type to save disk space.
|
||||||
@@ -52,7 +51,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-warning d-flex align-items-start" role="alert">
|
<div class="alert alert-warning d-flex align-items-start" role="alert">
|
||||||
<i class="bi bi-exclamation-triangle-fill me-2 mt-1"></i>
|
|
||||||
<div>
|
<div>
|
||||||
<strong>Warning:</strong> Existing files in the target Deploy directory with the
|
<strong>Warning:</strong> Existing files in the target Deploy directory with the
|
||||||
same names will be overwritten. This operation may take several minutes for large
|
same names will be overwritten. This operation may take several minutes for large
|
||||||
@@ -61,19 +59,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary" id="uploadImportBtn">
|
<button type="submit" class="btn btn-primary" id="uploadImportBtn">
|
||||||
<i class="bi bi-download me-1"></i> Start Import
|
Start Import
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center py-4">
|
<div class="text-center py-4">
|
||||||
<i class="bi bi-cloud-slash display-4 text-muted"></i>
|
|
||||||
<h5 class="mt-3 text-muted">No Upload Content Found</h5>
|
<h5 class="mt-3 text-muted">No Upload Content Found</h5>
|
||||||
<p class="text-muted mb-0">
|
<p class="text-muted mb-0">
|
||||||
Map <code>\\10.9.100.1\image-upload</code> on your Windows PC and copy
|
Map <code>\\10.9.100.1\image-upload</code> on your Windows PC and copy
|
||||||
the Deploy directory contents there.
|
the Deploy directory contents there.
|
||||||
</p>
|
</p>
|
||||||
<button class="btn btn-outline-secondary btn-sm mt-3" onclick="location.reload()">
|
<button class="btn btn-outline-secondary btn-sm mt-3" onclick="location.reload()">
|
||||||
<i class="bi bi-arrow-clockwise"></i> Refresh
|
Refresh
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -83,7 +80,7 @@
|
|||||||
<!-- USB Import -->
|
<!-- USB Import -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<i class="bi bi-usb-drive me-2"></i> Import from USB Drive
|
Import from USB Drive
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if usb_mounts %}
|
{% if usb_mounts %}
|
||||||
@@ -116,7 +113,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-warning d-flex align-items-start" role="alert">
|
<div class="alert alert-warning d-flex align-items-start" role="alert">
|
||||||
<i class="bi bi-exclamation-triangle-fill me-2 mt-1"></i>
|
|
||||||
<div>
|
<div>
|
||||||
<strong>Warning:</strong> Existing files in the target Deploy directory with the
|
<strong>Warning:</strong> Existing files in the target Deploy directory with the
|
||||||
same names will be overwritten. This operation may take several minutes for large
|
same names will be overwritten. This operation may take several minutes for large
|
||||||
@@ -125,19 +121,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary" id="importBtn">
|
<button type="submit" class="btn btn-primary" id="importBtn">
|
||||||
<i class="bi bi-download me-1"></i> Start Import
|
Start Import
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center py-4">
|
<div class="text-center py-4">
|
||||||
<i class="bi bi-usb-plug display-4 text-muted"></i>
|
|
||||||
<h5 class="mt-3 text-muted">No USB Drives Detected</h5>
|
<h5 class="mt-3 text-muted">No USB Drives Detected</h5>
|
||||||
<p class="text-muted mb-0">
|
<p class="text-muted mb-0">
|
||||||
No mounted USB drives were found under <code>/mnt/</code> or <code>/media/</code>.<br>
|
No mounted USB drives were found under <code>/mnt/</code> or <code>/media/</code>.<br>
|
||||||
Mount a USB drive and refresh this page.
|
Mount a USB drive and refresh this page.
|
||||||
</p>
|
</p>
|
||||||
<button class="btn btn-outline-secondary btn-sm mt-3" onclick="location.reload()">
|
<button class="btn btn-outline-secondary btn-sm mt-3" onclick="location.reload()">
|
||||||
<i class="bi bi-arrow-clockwise"></i> Refresh
|
Refresh
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -148,7 +143,7 @@
|
|||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<i class="bi bi-info-circle me-2"></i> Current Image Status
|
Current Image Status
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<table class="table table-sm mb-0">
|
<table class="table table-sm mb-0">
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h2 class="mb-0"><i class="bi bi-file-earmark-check me-2"></i>Blancco Erasure Reports</h2>
|
<h2 class="mb-0">Blancco Erasure Reports</h2>
|
||||||
<span class="badge bg-secondary fs-6">{{ reports|length }} report{{ 's' if reports|length != 1 }}</span>
|
<span class="badge bg-secondary fs-6">{{ reports|length }} report{{ 's' if reports|length != 1 }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header d-flex align-items-center">
|
<div class="card-header d-flex align-items-center">
|
||||||
<i class="bi bi-shield-check me-2"></i> Drive Erasure Certificates
|
Drive Erasure Certificates
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
{% if reports %}
|
{% if reports %}
|
||||||
@@ -39,12 +39,12 @@
|
|||||||
<td class="text-end text-nowrap">
|
<td class="text-end text-nowrap">
|
||||||
<a href="{{ url_for('blancco_download_report', filename=r.filename) }}"
|
<a href="{{ url_for('blancco_download_report', filename=r.filename) }}"
|
||||||
class="btn btn-sm btn-outline-primary" title="Download">
|
class="btn btn-sm btn-outline-primary" title="Download">
|
||||||
<i class="bi bi-download"></i>
|
Download
|
||||||
</a>
|
</a>
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger"
|
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||||
data-filename="{{ r.filename }}" title="Delete">
|
data-filename="{{ r.filename }}" title="Delete">
|
||||||
<i class="bi bi-trash"></i>
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -53,7 +53,6 @@
|
|||||||
</table>
|
</table>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center text-muted py-5">
|
<div class="text-center text-muted py-5">
|
||||||
<i class="bi bi-shield-check" style="font-size: 3rem;"></i>
|
|
||||||
<p class="mt-2">No erasure reports yet.</p>
|
<p class="mt-2">No erasure reports yet.</p>
|
||||||
<p class="small">Reports will appear here after Blancco Drive Eraser completes a wipe.</p>
|
<p class="small">Reports will appear here after Blancco Drive Eraser completes a wipe.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,7 +62,7 @@
|
|||||||
|
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h6 class="card-title"><i class="bi bi-info-circle me-1"></i> Report Storage</h6>
|
<h6 class="card-title">Report Storage</h6>
|
||||||
<p class="card-text mb-1">
|
<p class="card-text mb-1">
|
||||||
Blancco Drive Eraser saves erasure certificates to the network share
|
Blancco Drive Eraser saves erasure certificates to the network share
|
||||||
<code>\\10.9.100.1\blancco-reports</code>.
|
<code>\\10.9.100.1\blancco-reports</code>.
|
||||||
@@ -81,7 +80,7 @@
|
|||||||
<form id="deleteForm" method="post">
|
<form id="deleteForm" method="post">
|
||||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title"><i class="bi bi-exclamation-triangle me-2 text-danger"></i>Confirm Delete</h5>
|
<h5 class="modal-title">Confirm Delete</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@@ -90,7 +89,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
<button type="submit" class="btn btn-danger"><i class="bi bi-trash me-1"></i> Delete</button>
|
<button type="submit" class="btn btn-danger">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,12 +34,11 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h2 class="mb-0"><i class="bi bi-terminal me-2"></i>startnet.cmd Editor</h2>
|
<h2 class="mb-0">startnet.cmd Editor</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if not wim_exists %}
|
{% if not wim_exists %}
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
||||||
<strong>boot.wim not found</strong> at <code>{{ wim_path }}</code>.
|
<strong>boot.wim not found</strong> at <code>{{ wim_path }}</code>.
|
||||||
Run the PXE server setup playbook and import WinPE boot files first.
|
Run the PXE server setup playbook and import WinPE boot files first.
|
||||||
</div>
|
</div>
|
||||||
@@ -49,7 +48,7 @@
|
|||||||
<div class="col-lg-9">
|
<div class="col-lg-9">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<span><i class="bi bi-file-earmark-code me-2"></i>Windows\System32\startnet.cmd</span>
|
<span>Windows\System32\startnet.cmd</span>
|
||||||
<span class="badge bg-secondary">boot.wim</span>
|
<span class="badge bg-secondary">boot.wim</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
@@ -64,14 +63,14 @@
|
|||||||
Editing the startnet.cmd inside <code>{{ wim_path }}</code>
|
Editing the startnet.cmd inside <code>{{ wim_path }}</code>
|
||||||
</small>
|
</small>
|
||||||
<button type="submit" form="startnetForm" class="btn btn-primary">
|
<button type="submit" form="startnetForm" class="btn btn-primary">
|
||||||
<i class="bi bi-floppy me-1"></i> Save to boot.wim
|
Save to boot.wim
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h6 class="card-title"><i class="bi bi-info-circle me-1"></i> Common startnet.cmd Commands</h6>
|
<h6 class="card-title">Common startnet.cmd Commands</h6>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<code class="d-block mb-1">wpeinit</code>
|
<code class="d-block mb-1">wpeinit</code>
|
||||||
@@ -93,7 +92,7 @@
|
|||||||
<div class="col-lg-3">
|
<div class="col-lg-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<i class="bi bi-info-square me-2"></i>WIM Info
|
WIM Info
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body wim-info">
|
<div class="card-body wim-info">
|
||||||
<dl class="mb-0">
|
<dl class="mb-0">
|
||||||
|
|||||||
@@ -43,13 +43,12 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="mb-1">{{ friendly_name }}</h2>
|
<h2 class="mb-1">{{ friendly_name }}</h2>
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
<i class="bi bi-file-earmark-code me-1"></i>
|
|
||||||
<code>{{ image_type }}/Deploy/FlatUnattendW10.xml</code>
|
<code>{{ image_type }}/Deploy/FlatUnattendW10.xml</code>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button type="button" class="btn btn-success" id="saveFormBtn">
|
<button type="button" class="btn btn-success" id="saveFormBtn">
|
||||||
<i class="bi bi-floppy me-1"></i> Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,13 +58,13 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" id="form-tab" data-bs-toggle="tab"
|
<a class="nav-link active" id="form-tab" data-bs-toggle="tab"
|
||||||
href="#formView" role="tab">
|
href="#formView" role="tab">
|
||||||
<i class="bi bi-ui-checks-grid me-1"></i> Form Editor
|
Form Editor
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" id="raw-tab" data-bs-toggle="tab"
|
<a class="nav-link" id="raw-tab" data-bs-toggle="tab"
|
||||||
href="#rawView" role="tab">
|
href="#rawView" role="tab">
|
||||||
<i class="bi bi-code-slash me-1"></i> Raw XML
|
Raw XML
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -80,9 +79,9 @@
|
|||||||
<!-- 1. Driver Paths -->
|
<!-- 1. Driver Paths -->
|
||||||
<div class="card section-card">
|
<div class="card section-card">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<span><i class="bi bi-motherboard me-1"></i> Driver Paths</span>
|
<span>Driver Paths</span>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addDriverPath">
|
<button type="button" class="btn btn-sm btn-outline-primary" id="addDriverPath">
|
||||||
<i class="bi bi-plus-lg"></i> Add
|
Add
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
@@ -104,7 +103,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
|
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
|
||||||
<i class="bi bi-trash"></i>
|
Remove
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -122,7 +121,7 @@
|
|||||||
<!-- 2. Machine Settings -->
|
<!-- 2. Machine Settings -->
|
||||||
<div class="card section-card">
|
<div class="card section-card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<i class="bi bi-pc-display me-1"></i> Machine Settings
|
Machine Settings
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
@@ -154,9 +153,9 @@
|
|||||||
<!-- 3. Specialize Commands (RunSynchronous) -->
|
<!-- 3. Specialize Commands (RunSynchronous) -->
|
||||||
<div class="card section-card">
|
<div class="card section-card">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<span><i class="bi bi-terminal me-1"></i> Specialize Commands (RunSynchronous)</span>
|
<span>Specialize Commands (RunSynchronous)</span>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addSpecCmd">
|
<button type="button" class="btn btn-sm btn-outline-primary" id="addSpecCmd">
|
||||||
<i class="bi bi-plus-lg"></i> Add
|
Add
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
@@ -173,7 +172,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for cmd in data.specialize_commands %}
|
{% for cmd in data.specialize_commands %}
|
||||||
<tr draggable="true">
|
<tr draggable="true">
|
||||||
<td class="drag-handle"><i class="bi bi-grip-vertical"></i></td>
|
<td class="drag-handle">::</td>
|
||||||
<td class="order-num">{{ loop.index }}</td>
|
<td class="order-num">{{ loop.index }}</td>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" class="form-control form-control-sm"
|
<input type="text" class="form-control form-control-sm"
|
||||||
@@ -185,13 +184,13 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-nowrap">
|
<td class="text-nowrap">
|
||||||
<button type="button" class="btn btn-outline-secondary btn-row-action move-up" title="Move up">
|
<button type="button" class="btn btn-outline-secondary btn-row-action move-up" title="Move up">
|
||||||
<i class="bi bi-arrow-up"></i>
|
Up
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-secondary btn-row-action move-down" title="Move down">
|
<button type="button" class="btn btn-outline-secondary btn-row-action move-down" title="Move down">
|
||||||
<i class="bi bi-arrow-down"></i>
|
Down
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
|
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
|
||||||
<i class="bi bi-trash"></i>
|
Remove
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -209,7 +208,7 @@
|
|||||||
<!-- 4. OOBE Settings -->
|
<!-- 4. OOBE Settings -->
|
||||||
<div class="card section-card">
|
<div class="card section-card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<i class="bi bi-shield-check me-1"></i> OOBE Settings
|
OOBE Settings
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
@@ -260,12 +259,160 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 5. First Logon Commands -->
|
<!-- 5. User Accounts -->
|
||||||
<div class="card section-card">
|
<div class="card section-card">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<span><i class="bi bi-play-circle me-1"></i> First Logon Commands</span>
|
<span>User Accounts (Local)</span>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" id="addUserAccount">
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<table class="table table-sm mb-0" id="userAccountsTable">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width:40px">#</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Password</th>
|
||||||
|
<th>Group</th>
|
||||||
|
<th>Display Name</th>
|
||||||
|
<th style="width:60px"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for acct in data.user_accounts %}
|
||||||
|
<tr>
|
||||||
|
<td class="order-num">{{ loop.index }}</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" class="form-control form-control-sm"
|
||||||
|
name="account_name_{{ loop.index0 }}" value="{{ acct.name }}">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" class="form-control form-control-sm"
|
||||||
|
name="account_password_{{ loop.index0 }}" value="{{ acct.password }}">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" class="form-control form-control-sm"
|
||||||
|
name="account_group_{{ loop.index0 }}" value="{{ acct.group }}">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" class="form-control form-control-sm"
|
||||||
|
name="account_display_{{ loop.index0 }}" value="{{ acct.display_name }}">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="hidden" name="account_plaintext_{{ loop.index0 }}" value="{{ acct.plain_text }}">
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-row-action remove-account-row">
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% if not data.user_accounts %}
|
||||||
|
<div class="text-center text-muted py-3 empty-message" id="userAccountsEmpty">
|
||||||
|
No local accounts configured. Click <strong>Add</strong> to add one.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 6. AutoLogon -->
|
||||||
|
<div class="card section-card">
|
||||||
|
<div class="card-header">
|
||||||
|
AutoLogon
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="form-check form-switch mt-4">
|
||||||
|
<input class="form-check-input" type="checkbox" id="autologonEnabledToggle"
|
||||||
|
{% if data.autologon.enabled|lower == 'true' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label fw-semibold" for="autologonEnabledToggle">Enabled</label>
|
||||||
|
<input type="hidden" name="autologon_enabled" id="autologon_enabled_val"
|
||||||
|
value="{{ data.autologon.enabled }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label fw-semibold">Username</label>
|
||||||
|
<input type="text" class="form-control" name="autologon_username"
|
||||||
|
value="{{ data.autologon.username }}" placeholder="e.g. SupportUser">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label fw-semibold">Password</label>
|
||||||
|
<input type="text" class="form-control" name="autologon_password"
|
||||||
|
value="{{ data.autologon.password }}">
|
||||||
|
<input type="hidden" name="autologon_plaintext" value="{{ data.autologon.plain_text }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label fw-semibold">Logon Count</label>
|
||||||
|
<input type="text" class="form-control" name="autologon_logoncount"
|
||||||
|
value="{{ data.autologon.logon_count }}" placeholder="e.g. 2 or 999">
|
||||||
|
<div class="form-text">Number of auto-logon attempts.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 7. International Settings -->
|
||||||
|
<div class="card section-card">
|
||||||
|
<div class="card-header" data-bs-toggle="collapse" data-bs-target="#intlCollapse"
|
||||||
|
role="button" style="cursor:pointer">
|
||||||
|
International Settings
|
||||||
|
<small class="text-muted ms-2">(click to expand)</small>
|
||||||
|
</div>
|
||||||
|
<div class="collapse {% if data.intl.input_locale or data.intl.system_locale or data.intl.ui_language or data.intl.user_locale %}show{% endif %}"
|
||||||
|
id="intlCollapse">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label fw-semibold">Input Locale</label>
|
||||||
|
<input type="text" class="form-control" name="intl_input_locale"
|
||||||
|
value="{{ data.intl.input_locale }}" placeholder="e.g. en-US">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label fw-semibold">System Locale</label>
|
||||||
|
<input type="text" class="form-control" name="intl_system_locale"
|
||||||
|
value="{{ data.intl.system_locale }}" placeholder="e.g. en-US">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label fw-semibold">UI Language</label>
|
||||||
|
<input type="text" class="form-control" name="intl_ui_language"
|
||||||
|
value="{{ data.intl.ui_language }}" placeholder="e.g. en-US">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label fw-semibold">User Locale</label>
|
||||||
|
<input type="text" class="form-control" name="intl_user_locale"
|
||||||
|
value="{{ data.intl.user_locale }}" placeholder="e.g. en-US">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 8. OOBE Time Zone -->
|
||||||
|
<div class="card section-card">
|
||||||
|
<div class="card-header">
|
||||||
|
OOBE Time Zone
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold">Time Zone (oobeSystem pass)</label>
|
||||||
|
<input type="text" class="form-control" name="oobe_timezone"
|
||||||
|
value="{{ data.oobe_timezone }}" placeholder="e.g. Eastern Standard Time">
|
||||||
|
<div class="form-text">Separate from the specialize-pass time zone in Machine Settings above.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 9. First Logon Commands -->
|
||||||
|
<div class="card section-card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<span>First Logon Commands</span>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addFlCmd">
|
<button type="button" class="btn btn-sm btn-outline-primary" id="addFlCmd">
|
||||||
<i class="bi bi-plus-lg"></i> Add
|
Add
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
@@ -282,7 +429,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for cmd in data.firstlogon_commands %}
|
{% for cmd in data.firstlogon_commands %}
|
||||||
<tr draggable="true">
|
<tr draggable="true">
|
||||||
<td class="drag-handle"><i class="bi bi-grip-vertical"></i></td>
|
<td class="drag-handle">::</td>
|
||||||
<td class="order-num">{{ loop.index }}</td>
|
<td class="order-num">{{ loop.index }}</td>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" class="form-control form-control-sm"
|
<input type="text" class="form-control form-control-sm"
|
||||||
@@ -294,13 +441,13 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-nowrap">
|
<td class="text-nowrap">
|
||||||
<button type="button" class="btn btn-outline-secondary btn-row-action move-up" title="Move up">
|
<button type="button" class="btn btn-outline-secondary btn-row-action move-up" title="Move up">
|
||||||
<i class="bi bi-arrow-up"></i>
|
Up
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-secondary btn-row-action move-down" title="Move down">
|
<button type="button" class="btn btn-outline-secondary btn-row-action move-down" title="Move down">
|
||||||
<i class="bi bi-arrow-down"></i>
|
Down
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
|
<button type="button" class="btn btn-outline-danger btn-row-action remove-row">
|
||||||
<i class="bi bi-trash"></i>
|
Remove
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -321,9 +468,9 @@
|
|||||||
<div class="tab-pane fade" id="rawView" role="tabpanel">
|
<div class="tab-pane fade" id="rawView" role="tabpanel">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<span><i class="bi bi-code-slash me-1"></i> Raw XML</span>
|
<span>Raw XML</span>
|
||||||
<button type="button" class="btn btn-sm btn-success" id="saveRawBtn">
|
<button type="button" class="btn btn-sm btn-success" id="saveRawBtn">
|
||||||
<i class="bi bi-floppy me-1"></i> Save Raw XML
|
Save Raw XML
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|||||||
Reference in New Issue
Block a user