Initial import: shopdb-flask multi-site asset platform
Flask + Vue rewrite of the classic-ASP shopdb, built as a plugin framework so sister GE Aerospace sites can adopt it. Product version 0.7.0-pre-0.8.0, plugin contract 0.11.0, 966 backend tests. Key entry points: - docs/INSTALL-WINDOWS-IIS.md - tested Windows + IIS install runbook (own-site and subpath deployment methods) - docs/PILOT-DEPLOY.md - production pilot: stand up + import + verify - docs/IMPORT-API.md - schema-agnostic legacy data import contract - scripts/site_imports/wjf/ - West Jefferson reference import loader - docs/adr/ - twelve accepted architecture decision records
This commit is contained in:
180
plugins/geenforce/client/Install-GEEnforce.ps1
Normal file
180
plugins/geenforce/client/Install-GEEnforce.ps1
Normal file
@@ -0,0 +1,180 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Provision a PC for GE-Enforce + shopdb: write its identity (so it knows its PC
|
||||
type and bay), deploy the shopdb client kit, and register the enforcement
|
||||
scheduled task. Site-neutral and imaging-path independent - run it from a PXE
|
||||
step, an OOBE provisioning package (ppkg), Intune, or by hand.
|
||||
|
||||
This is the "set up the PC to determine its PC type and such" step. There is no
|
||||
auto-detection: the caller supplies the type (and, where relevant, the bay
|
||||
machine number and CMM version), and this script writes the files/registry the
|
||||
GE-Enforce engine reads.
|
||||
|
||||
This script does NOT contain the GE-Enforce engine (Install-FromManifest.ps1)
|
||||
or dispatcher - those are the GE-Enforce framework's. Point at your copy with
|
||||
-EngineSource, or ensure it is already present under -InstallRoot\lib.
|
||||
|
||||
.PARAMETER PCType
|
||||
The imaging PC type = the manifest scope this PC runs. One of the scope names
|
||||
(e.g. gea-shopfloor-cmm, gea-shopfloor-collections) or a legacy alias the
|
||||
engine maps. Written to C:\Enrollment\pc-type.txt. REQUIRED.
|
||||
|
||||
.PARAMETER MachineNumber
|
||||
The bay machine number (for TargetMachineNumbers gates). Written to
|
||||
machine-number.txt as a fallback; the DNC registry MachineNo wins if present.
|
||||
|
||||
.PARAMETER CmmVersion
|
||||
CMM bays only: the resolved PC-DMIS version (2016/2019/2026) for _CmmVersion
|
||||
gating. Written to C:\Enrollment\cmm\version.txt. Omit for non-CMM PCs.
|
||||
|
||||
.PARAMETER CmmId
|
||||
CMM bays only: the bay id. Written to C:\Enrollment\cmm\cmmid.txt.
|
||||
|
||||
.PARAMETER ShareRoot
|
||||
The GE-Enforce share root (UNC), written into site-config.json as
|
||||
shopfloorShareRoot.
|
||||
|
||||
.PARAMETER Site
|
||||
Site name, written into site-config.json.
|
||||
|
||||
.PARAMETER ShopdbUrl
|
||||
shopdb base URL (scheme + host) for the fetch/report client. Written to
|
||||
HKLM:\SOFTWARE\GE\ShopDB\BaseUrl.
|
||||
|
||||
.PARAMETER ShopdbToken
|
||||
A geenforce.fetch (+ geenforce.report) managed service token. Written to
|
||||
HKLM:\SOFTWARE\GE\ShopDB\ApiToken. (In many sites Azure DSC provisions this
|
||||
instead - omit here if so.)
|
||||
|
||||
.PARAMETER EngineSource
|
||||
Optional path (folder or share) containing the GE-Enforce engine to copy in:
|
||||
expects GE-Enforce.ps1 and lib\Install-FromManifest.ps1. If omitted, the engine
|
||||
is assumed already present under -InstallRoot.
|
||||
|
||||
.PARAMETER InstallRoot
|
||||
Where the client kit + engine live on the PC. Default C:\ProgramData\GE-Enforce.
|
||||
|
||||
.PARAMETER TaskName
|
||||
Scheduled task name. Default 'GE-Enforce'.
|
||||
|
||||
.PARAMETER IntervalMinutes
|
||||
How often the enforcement task repeats. Default 5.
|
||||
|
||||
.PARAMETER NoTask
|
||||
Provision identity + kit only; do not register the scheduled task.
|
||||
|
||||
.EXAMPLE
|
||||
.\Install-GEEnforce.ps1 -PCType gea-shopfloor-cmm -MachineNumber 0615 `
|
||||
-CmmVersion 2019 -ShareRoot \\server\share\dt\shopfloor -Site "West Jefferson" `
|
||||
-ShopdbUrl https://shopdb.site.geaerospace.net -ShopdbToken shopdb_pat_xxx `
|
||||
-EngineSource \\server\share\dt\shopfloor\common
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)] [string]$PCType,
|
||||
[string]$MachineNumber,
|
||||
[string]$CmmVersion,
|
||||
[string]$CmmId,
|
||||
[string]$ShareRoot,
|
||||
[string]$Site,
|
||||
[string]$ShopdbUrl,
|
||||
[string]$ShopdbToken,
|
||||
[string]$EngineSource,
|
||||
[string]$InstallRoot = 'C:\ProgramData\GE-Enforce',
|
||||
[string]$EnrollmentRoot = 'C:\Enrollment',
|
||||
[string]$TaskName = 'GE-Enforce',
|
||||
[int]$IntervalMinutes = 5,
|
||||
[switch]$NoTask
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Write-TextFile {
|
||||
param([string]$Path, [string]$Value)
|
||||
$dir = Split-Path -Parent $Path
|
||||
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
|
||||
Set-Content -LiteralPath $Path -Value $Value -Encoding ascii -NoNewline
|
||||
Write-Host " wrote $Path"
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Host "GE-Enforce provisioning: PCType=$PCType"
|
||||
|
||||
# --- 1. Identity: what the PC is (what the engine reads) -----------------
|
||||
Write-TextFile (Join-Path $EnrollmentRoot 'pc-type.txt') $PCType
|
||||
if ($MachineNumber) { Write-TextFile (Join-Path $EnrollmentRoot 'machine-number.txt') $MachineNumber }
|
||||
if ($CmmVersion) { Write-TextFile (Join-Path $EnrollmentRoot 'cmm\version.txt') $CmmVersion }
|
||||
if ($CmmId) { Write-TextFile (Join-Path $EnrollmentRoot 'cmm\cmmid.txt') $CmmId }
|
||||
|
||||
$siteConfig = @{}
|
||||
if ($ShareRoot) { $siteConfig['shopfloorShareRoot'] = $ShareRoot }
|
||||
if ($Site) { $siteConfig['site'] = $Site }
|
||||
if ($siteConfig.Count) {
|
||||
$siteConfigPath = Join-Path $EnrollmentRoot 'site-config.json'
|
||||
$dir = Split-Path -Parent $siteConfigPath
|
||||
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
|
||||
($siteConfig | ConvertTo-Json) | Set-Content -LiteralPath $siteConfigPath -Encoding ascii
|
||||
Write-Host " wrote $siteConfigPath"
|
||||
}
|
||||
|
||||
# --- 2. shopdb client registry config ------------------------------------
|
||||
if ($ShopdbUrl -or $ShopdbToken) {
|
||||
$regPath = 'HKLM:\SOFTWARE\GE\ShopDB'
|
||||
if (-not (Test-Path $regPath)) { New-Item -Path $regPath -Force | Out-Null }
|
||||
if ($ShopdbUrl) { Set-ItemProperty -Path $regPath -Name BaseUrl -Value $ShopdbUrl }
|
||||
if ($ShopdbToken) { Set-ItemProperty -Path $regPath -Name ApiToken -Value $ShopdbToken }
|
||||
Write-Host " wrote HKLM:\SOFTWARE\GE\ShopDB"
|
||||
}
|
||||
|
||||
# --- 3. Deploy the client kit (ships alongside this script) --------------
|
||||
if (-not (Test-Path $InstallRoot)) { New-Item -ItemType Directory -Path $InstallRoot -Force | Out-Null }
|
||||
foreach ($file in 'ShopdbEnforceClient.psm1', 'Invoke-ShopdbEnforce.ps1') {
|
||||
$src = Join-Path $PSScriptRoot $file
|
||||
if (Test-Path $src) {
|
||||
Copy-Item -Path $src -Destination (Join-Path $InstallRoot $file) -Force
|
||||
Write-Host " deployed $file"
|
||||
} else {
|
||||
Write-Warning "client kit file not found next to this script: $file"
|
||||
}
|
||||
}
|
||||
|
||||
# --- 4. Engine (referenced, not vendored) --------------------------------
|
||||
if ($EngineSource) {
|
||||
Copy-Item -Path (Join-Path $EngineSource 'GE-Enforce.ps1') `
|
||||
-Destination (Join-Path $InstallRoot 'GE-Enforce.ps1') -Force -ErrorAction SilentlyContinue
|
||||
$libDir = Join-Path $InstallRoot 'lib'
|
||||
if (-not (Test-Path $libDir)) { New-Item -ItemType Directory -Path $libDir -Force | Out-Null }
|
||||
Copy-Item -Path (Join-Path $EngineSource 'lib\Install-FromManifest.ps1') `
|
||||
-Destination (Join-Path $libDir 'Install-FromManifest.ps1') -Force -ErrorAction SilentlyContinue
|
||||
Write-Host " copied engine from $EngineSource"
|
||||
}
|
||||
$enginePath = Join-Path $InstallRoot 'lib\Install-FromManifest.ps1'
|
||||
if (-not (Test-Path $enginePath)) {
|
||||
Write-Warning "engine not present at $enginePath - provide -EngineSource or place it there before enforcement runs."
|
||||
}
|
||||
|
||||
# --- 5. Scheduled task: run the enforcement client as SYSTEM -------------
|
||||
if (-not $NoTask) {
|
||||
$runner = Join-Path $InstallRoot 'Invoke-ShopdbEnforce.ps1'
|
||||
$arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$runner`" -Scope `"$PCType`" -EnginePath `"$enginePath`""
|
||||
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $arguments
|
||||
$triggers = @(
|
||||
(New-ScheduledTaskTrigger -AtLogOn),
|
||||
(New-ScheduledTaskTrigger -Once -At (Get-Date) `
|
||||
-RepetitionInterval (New-TimeSpan -Minutes $IntervalMinutes))
|
||||
)
|
||||
$principal = New-ScheduledTaskPrincipal -UserId 'NT AUTHORITY\SYSTEM' -RunLevel Highest
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries -StartWhenAvailable
|
||||
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $triggers `
|
||||
-Principal $principal -Settings $settings -Force | Out-Null
|
||||
Write-Host " registered task '$TaskName' (at logon + every $IntervalMinutes min)"
|
||||
}
|
||||
|
||||
Write-Host "GE-Enforce provisioning complete."
|
||||
exit 0
|
||||
} catch {
|
||||
Write-Error "GE-Enforce provisioning FAILED: $($_.Exception.Message)"
|
||||
exit 1
|
||||
}
|
||||
103
plugins/geenforce/client/Invoke-ShopdbEnforce.ps1
Normal file
103
plugins/geenforce/client/Invoke-ShopdbEnforce.ps1
Normal file
@@ -0,0 +1,103 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Reference orchestrator: source a GE-Enforce manifest from shopdb, run the
|
||||
UNCHANGED engine against it, and report the result back to shopdb.
|
||||
|
||||
This is a thin wrapper around Install-FromManifest.ps1 (the engine). It is a
|
||||
reference a site adapts into its GE-Enforce.ps1 flow; it is not the live
|
||||
dispatcher. The engine, detection, self-heal, and SMB payload resolution are
|
||||
untouched - only the source of the manifest JSON moves from a share file to
|
||||
shopdb, plus a result report.
|
||||
|
||||
.PARAMETER Scope
|
||||
The imaging pc-type / scope name (e.g. gea-shopfloor-cmm), same value the
|
||||
dispatcher already resolves from C:\Enrollment\pc-type.txt.
|
||||
|
||||
.PARAMETER EnginePath
|
||||
Path to Install-FromManifest.ps1 (the engine lib, >= 2.6).
|
||||
|
||||
.PARAMETER ShareManifestPath
|
||||
The current on-share manifest for this scope. In shadow mode the engine runs
|
||||
against THIS (unchanged behavior) and shopdb is only compared + reported. Once
|
||||
cut over, omit it and the engine runs against the shopdb-sourced manifest.
|
||||
|
||||
.PARAMETER ShadowMode
|
||||
Fetch + compare + report, but install from the share (no behavior change).
|
||||
|
||||
.NOTES
|
||||
Fail-safe: any error exits 0 so a bad web app never blocks or breaks a PC.
|
||||
Config comes from HKLM:\SOFTWARE\GE\ShopDB (BaseUrl, ApiToken) - see the psm1.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)] [string]$Scope,
|
||||
[Parameter(Mandatory)] [string]$EnginePath,
|
||||
[string]$ShareManifestPath,
|
||||
[switch]$ShadowMode,
|
||||
[string]$BaseUrl,
|
||||
[string]$ApiToken,
|
||||
[string]$LogFile = "C:\Logs\Shopfloor\shopdb-enforce-$(Get-Date -Format yyyyMMdd).log"
|
||||
)
|
||||
|
||||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = 'INFO')
|
||||
$line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [$Level] $Message"
|
||||
try { Add-Content -LiteralPath $LogFile -Value $line -ErrorAction SilentlyContinue } catch {}
|
||||
Write-Host $line
|
||||
}
|
||||
|
||||
try {
|
||||
Import-Module (Join-Path $PSScriptRoot 'ShopdbEnforceClient.psm1') -Force
|
||||
|
||||
$config = Get-ShopdbConfig -BaseUrl $BaseUrl -ApiToken $ApiToken
|
||||
if (-not $config) {
|
||||
Write-Log 'No shopdb BaseUrl/ApiToken configured yet - retry next cycle.' 'WARN'
|
||||
exit 0
|
||||
}
|
||||
|
||||
$sync = Sync-ShopdbManifest -Scope $Scope -Config $config
|
||||
if (-not $sync.Path) {
|
||||
Write-Log "No manifest available for $Scope (shopdb unreachable, no cache)." 'WARN'
|
||||
exit 0
|
||||
}
|
||||
Write-Log "Manifest for $Scope from $($sync.Source) (v$($sync.Version))."
|
||||
|
||||
# Shadow mode: compare shopdb vs the share, but install from the share.
|
||||
if ($ShadowMode -and $ShareManifestPath -and (Test-Path $ShareManifestPath)) {
|
||||
$diff = Compare-ShopdbShadow -ShopdbManifestPath $sync.Path -ShareManifestPath $ShareManifestPath
|
||||
if ($diff.Same) {
|
||||
Write-Log 'Shadow: shopdb manifest matches the share.'
|
||||
} else {
|
||||
Write-Log ("Shadow DIFF: shopdb-only=[{0}] share-only=[{1}] orderDiff={2}" -f `
|
||||
($diff.ShopdbOnly -join ','), ($diff.ShareOnly -join ','), $diff.OrderDiff) 'WARN'
|
||||
}
|
||||
}
|
||||
|
||||
# Which manifest the engine actually runs against.
|
||||
$manifestToRun = if ($ShadowMode -and $ShareManifestPath) { $ShareManifestPath } else { $sync.Path }
|
||||
|
||||
# --- INTEGRATION POINT ---------------------------------------------------
|
||||
# Run the engine. Install-FromManifest.ps1 is expected to return (or you
|
||||
# adapt it to return) a summary carrying Installed/Skipped/Failed/Filtered
|
||||
# and a Results list of @{Name;Action;SelfHealed;ExitCode;Message}. Wire this
|
||||
# to your engine's actual return/parse; the shape below is the contract.
|
||||
Write-Log "Running engine against $manifestToRun"
|
||||
$summary = & $EnginePath -ManifestPath $manifestToRun -PCType $Scope
|
||||
if (-not $summary) {
|
||||
$summary = @{ Installed = 0; Skipped = 0; Failed = 0; Filtered = 0; Results = @() }
|
||||
}
|
||||
if (-not $summary.EnforcerVersion) { $summary.EnforcerVersion = '2.6' }
|
||||
|
||||
# Report the result (best-effort).
|
||||
$appliedVersion = 0
|
||||
if ($sync.Version) { [int]::TryParse($sync.Version, [ref]$appliedVersion) | Out-Null }
|
||||
$report = New-ShopdbReport -Scope $Scope -AppliedVersion $appliedVersion -Summary $summary
|
||||
if (Send-ShopdbReport -Config $config -Report $report) {
|
||||
Write-Log "Reported: installed=$($summary.Installed) skipped=$($summary.Skipped) failed=$($summary.Failed)."
|
||||
} else {
|
||||
Write-Log 'Report POST failed (non-fatal).' 'WARN'
|
||||
}
|
||||
} catch {
|
||||
Write-Log "Unhandled error (non-fatal): $($_.Exception.Message)" 'ERROR'
|
||||
}
|
||||
exit 0
|
||||
164
plugins/geenforce/client/ShopdbEnforceClient.psm1
Normal file
164
plugins/geenforce/client/ShopdbEnforceClient.psm1
Normal file
@@ -0,0 +1,164 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Client-side helpers for sourcing GE-Enforce manifests from shopdb and reporting
|
||||
results back. Site-neutral reference kit - deploy alongside GE-Enforce; do NOT
|
||||
hardcode any site here.
|
||||
|
||||
This module does NOT replace Install-FromManifest.ps1 (the engine). It only
|
||||
changes where the manifest JSON comes from (shopdb HTTP instead of a share
|
||||
file) and adds a result report. The engine, detection, self-heal, and SMB
|
||||
payload resolution stay exactly as they are.
|
||||
|
||||
Resilience mirrors GE-Enforce: any failure is non-fatal (fail-safe). If shopdb
|
||||
is unreachable the client enforces from the last-known-good cached manifest and
|
||||
a PC is never left unmanaged because the web app is down.
|
||||
|
||||
Config (params override registry): HKLM:\SOFTWARE\GE\ShopDB
|
||||
BaseUrl e.g. https://shopdb.example.geaerospace.net
|
||||
ApiToken a geenforce.fetch (+ geenforce.report) managed service token,
|
||||
provisioned the same way as SFLD creds (Azure DSC).
|
||||
#>
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
function Get-ShopdbConfig {
|
||||
param([string]$BaseUrl, [string]$ApiToken)
|
||||
$regPath = 'HKLM:\SOFTWARE\GE\ShopDB'
|
||||
if ((-not $BaseUrl -or -not $ApiToken) -and (Test-Path $regPath)) {
|
||||
$props = Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue
|
||||
if (-not $BaseUrl -and $props.BaseUrl) { $BaseUrl = $props.BaseUrl }
|
||||
if (-not $ApiToken -and $props.ApiToken) { $ApiToken = $props.ApiToken }
|
||||
}
|
||||
if (-not $BaseUrl -or -not $ApiToken) { return $null }
|
||||
return @{ BaseUrl = $BaseUrl.TrimEnd('/'); ApiToken = $ApiToken }
|
||||
}
|
||||
|
||||
function Sync-ShopdbManifest {
|
||||
<#
|
||||
Fetch the current published manifest for a scope into a local cache, using
|
||||
an ETag so an unchanged manifest is a cheap 304. On any network error, fall
|
||||
back to the last-known-good cached copy. Returns:
|
||||
@{ Path; Version; Source } where Source is
|
||||
'shopdb' | 'cache-304' | 'cache-lastgood' | $null (nothing available)
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)] [string]$Scope,
|
||||
[Parameter(Mandatory)] [hashtable]$Config,
|
||||
[string]$CacheDir = 'C:\ProgramData\ShopDB\geenforce'
|
||||
)
|
||||
if (-not (Test-Path $CacheDir)) { New-Item -ItemType Directory -Path $CacheDir -Force | Out-Null }
|
||||
$manifestPath = Join-Path $CacheDir "$Scope.json"
|
||||
$etagPath = Join-Path $CacheDir "$Scope.etag"
|
||||
$headers = @{ 'X-API-Key' = $Config.ApiToken }
|
||||
if (Test-Path $etagPath) { $headers['If-None-Match'] = (Get-Content -LiteralPath $etagPath -Raw).Trim() }
|
||||
|
||||
$uri = "$($Config.BaseUrl)/api/geenforce/manifest?pctype=$([uri]::EscapeDataString($Scope))"
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri $uri -Headers $headers -UseBasicParsing `
|
||||
-TimeoutSec 30 -ErrorAction Stop
|
||||
if ($response.StatusCode -eq 200) {
|
||||
[System.IO.File]::WriteAllText($manifestPath, $response.Content)
|
||||
# PowerShell 7 returns header values as string arrays; 5.1 as scalars.
|
||||
# @(...)[0] yields a clean scalar in both.
|
||||
$etag = @($response.Headers['ETag'])[0]
|
||||
if ($etag) {
|
||||
Set-Content -LiteralPath $etagPath -Value $etag -NoNewline
|
||||
}
|
||||
$version = @($response.Headers['X-Manifest-Version'])[0]
|
||||
if ($version) {
|
||||
Set-Content -LiteralPath (Join-Path $CacheDir "$Scope.version") -Value $version -NoNewline
|
||||
}
|
||||
return @{ Path = $manifestPath; Version = $version; Source = 'shopdb' }
|
||||
}
|
||||
} catch {
|
||||
$status = $null
|
||||
if ($_.Exception.Response) { $status = [int]$_.Exception.Response.StatusCode }
|
||||
if ($status -eq 304 -and (Test-Path $manifestPath)) {
|
||||
return @{ Path = $manifestPath; Version = (Read-CachedVersion $CacheDir $Scope); Source = 'cache-304' }
|
||||
}
|
||||
# Network/other failure: fall back to last-known-good.
|
||||
if (Test-Path $manifestPath) {
|
||||
return @{ Path = $manifestPath; Version = (Read-CachedVersion $CacheDir $Scope); Source = 'cache-lastgood' }
|
||||
}
|
||||
return @{ Path = $null; Version = $null; Source = $null }
|
||||
}
|
||||
# 304 without exception (some PS versions) -> use cache.
|
||||
if (Test-Path $manifestPath) {
|
||||
return @{ Path = $manifestPath; Version = (Read-CachedVersion $CacheDir $Scope); Source = 'cache-304' }
|
||||
}
|
||||
return @{ Path = $null; Version = $null; Source = $null }
|
||||
}
|
||||
|
||||
function Read-CachedVersion {
|
||||
param([string]$CacheDir, [string]$Scope)
|
||||
$verPath = Join-Path $CacheDir "$Scope.version"
|
||||
if (Test-Path $verPath) { return (Get-Content -LiteralPath $verPath -Raw).Trim() }
|
||||
return $null
|
||||
}
|
||||
|
||||
function Compare-ShopdbShadow {
|
||||
<#
|
||||
Shadow-mode comparison: do the shopdb manifest and the on-share manifest
|
||||
select the same ordered entry names? Returns @{ Same; ShopdbOnly; ShareOnly;
|
||||
OrderDiff }. Behavioral, not byte, comparison.
|
||||
#>
|
||||
param([Parameter(Mandatory)][string]$ShopdbManifestPath,
|
||||
[Parameter(Mandatory)][string]$ShareManifestPath)
|
||||
$shopdb = (Get-Content -LiteralPath $ShopdbManifestPath -Raw | ConvertFrom-Json)
|
||||
$share = (Get-Content -LiteralPath $ShareManifestPath -Raw | ConvertFrom-Json)
|
||||
$shopdbNames = @($shopdb.Applications | ForEach-Object { $_.Name })
|
||||
$shareNames = @($share.Applications | ForEach-Object { $_.Name })
|
||||
return @{
|
||||
Same = (($shopdbNames -join '|') -eq ($shareNames -join '|'))
|
||||
ShopdbOnly = @($shopdbNames | Where-Object { $_ -notin $shareNames })
|
||||
ShareOnly = @($shareNames | Where-Object { $_ -notin $shopdbNames })
|
||||
OrderDiff = (($shopdbNames -join '|') -ne ($shareNames -join '|'))
|
||||
}
|
||||
}
|
||||
|
||||
function Send-ShopdbReport {
|
||||
<#
|
||||
POST an enforcement report to shopdb. Best-effort: never throws, returns
|
||||
$true on success. Report is a hashtable matching POST /api/geenforce/report.
|
||||
#>
|
||||
param([Parameter(Mandatory)][hashtable]$Config,
|
||||
[Parameter(Mandatory)][hashtable]$Report)
|
||||
try {
|
||||
$body = ($Report | ConvertTo-Json -Depth 6)
|
||||
Invoke-RestMethod -Uri "$($Config.BaseUrl)/api/geenforce/report" `
|
||||
-Method Post -Headers @{ 'X-API-Key' = $Config.ApiToken } `
|
||||
-ContentType 'application/json' -Body $body -TimeoutSec 30 -ErrorAction Stop | Out-Null
|
||||
return $true
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function New-ShopdbReport {
|
||||
<#
|
||||
Build a report payload from an engine summary. `Summary` is expected to
|
||||
carry Installed/Skipped/Failed/Filtered counts and a Results list of
|
||||
@{ Name; Action; SelfHealed; ExitCode; Message }. Shape the engine's own
|
||||
per-entry outcomes into this at the call site.
|
||||
#>
|
||||
param([string]$Hostname = $env:COMPUTERNAME,
|
||||
[Parameter(Mandatory)][string]$Scope,
|
||||
[int]$AppliedVersion,
|
||||
[Parameter(Mandatory)][hashtable]$Summary)
|
||||
return @{
|
||||
hostname = $Hostname
|
||||
scopename = $Scope
|
||||
appliedversion = $AppliedVersion
|
||||
enforcerversion = $Summary.EnforcerVersion
|
||||
counts = @{
|
||||
installed = [int]$Summary.Installed
|
||||
skipped = [int]$Summary.Skipped
|
||||
failed = [int]$Summary.Failed
|
||||
filtered = [int]$Summary.Filtered
|
||||
}
|
||||
results = @($Summary.Results)
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Get-ShopdbConfig, Sync-ShopdbManifest, `
|
||||
Compare-ShopdbShadow, Send-ShopdbReport, New-ShopdbReport, Read-CachedVersion
|
||||
Reference in New Issue
Block a user