Kiosks report their display type; whole-chain DDL parity

This commit is contained in:
2026-08-12 17:07:21 -04:00
parent 50578f33bd
commit d6b433e5f4
3 changed files with 82 additions and 11 deletions

View File

@@ -210,7 +210,8 @@ function New-ShopdbReport {
param([string]$Hostname = $env:COMPUTERNAME, param([string]$Hostname = $env:COMPUTERNAME,
[Parameter(Mandatory)][string]$Scope, [Parameter(Mandatory)][string]$Scope,
[int]$AppliedVersion, [int]$AppliedVersion,
[Parameter(Mandatory)][hashtable]$Summary) [Parameter(Mandatory)][hashtable]$Summary,
[string]$SubType = (Get-ShopdbSubType))
$results = @(foreach ($entry in @($Summary.Results)) { $results = @(foreach ($entry in @($Summary.Results)) {
if ($null -eq $entry) { continue } if ($null -eq $entry) { continue }
$mapped = @{ $mapped = @{
@@ -230,6 +231,7 @@ function New-ShopdbReport {
scopename = $Scope scopename = $Scope
appliedversion = $AppliedVersion appliedversion = $AppliedVersion
enforcerversion = $Summary.EnforcerVersion enforcerversion = $Summary.EnforcerVersion
subtype = $SubType
counts = @{ counts = @{
installed = [int]$Summary.Installed installed = [int]$Summary.Installed
skipped = [int]$Summary.Skipped skipped = [int]$Summary.Skipped
@@ -240,6 +242,32 @@ function New-ShopdbReport {
} }
} }
function Get-ShopdbSubType {
<#
.SYNOPSIS
What this PC is WITHIN its scope, read off the machine itself.
.DESCRIPTION
A display knows whether it is a Dashboard, a Lobby screen or the 3D print
room: the dispatcher reads C:\Enrollment\display-type.txt to choose which
page to open. It just never told shopdb, so the fleet table had to guess
from a DashboardDefault fqdn mapping that is empty unless somebody added a
row per kiosk. Reported by the device beats inferred from a lookup table,
the same way enforcerversion already works.
Returns '' when the file is absent - every non-display PC type - so the
report simply carries no subtype rather than a made-up one.
#>
param([string]$Path = 'C:\Enrollment\display-type.txt')
try {
if (-not (Test-Path $Path)) { return '' }
$value = (Get-Content -Path $Path -TotalCount 1 -ErrorAction Stop)
return ([string]$value).Trim()
} catch {
return ''
}
}
function Get-ShopdbPayload { function Get-ShopdbPayload {
<# <#
Fetch a payload blob by content hash over HTTPS, verify the sha256, and Fetch a payload blob by content hash over HTTPS, verify the sha256, and
@@ -449,4 +477,4 @@ function ConvertTo-ShopdbSummary {
Export-ModuleMember -Function Get-ShopdbConfig, Sync-ShopdbManifest, ` Export-ModuleMember -Function Get-ShopdbConfig, Sync-ShopdbManifest, `
Compare-ShopdbShadow, Send-ShopdbReport, New-ShopdbReport, Read-CachedVersion, ` Compare-ShopdbShadow, Send-ShopdbReport, New-ShopdbReport, Read-CachedVersion, `
Get-ShopdbPayload, Resolve-ShopdbPayloads, Merge-ShopdbManifests, ` Get-ShopdbPayload, Resolve-ShopdbPayloads, Merge-ShopdbManifests, `
ConvertTo-ShopdbSummary ConvertTo-ShopdbSummary, Get-ShopdbSubType

View File

@@ -22,9 +22,9 @@ from alembic.migration import MigrationContext
from alembic.operations import Operations from alembic.operations import Operations
_BASELINE = (Path(__file__).resolve().parent.parent.parent / 'plugins' / _VERSIONS = (Path(__file__).resolve().parent.parent.parent / 'plugins' /
'geenforce' / 'migrations' / 'versions' / 'geenforce' / 'migrations' / 'versions')
'0001_geenforce_baseline.py') _BASELINE = _VERSIONS / '0001_geenforce_baseline.py'
def _geenforce_models(): def _geenforce_models():
@@ -53,15 +53,23 @@ def _model_schema(db, models):
def _migration_schema(): def _migration_schema():
"""Run the baseline migration's upgrade() on a fresh engine; return columns. """Run the WHOLE chain's upgrade() on a fresh engine; return columns.
The whole chain, not just the baseline. Reading only 0001 meant a column
added by a later revision read as model/migration drift even though the
migration for it existed - the check is "do the models match what the
migrations build", and that is the chain, not its first file.
Returns {tablename: {columnname: reflected-column-dict}}. Reflection happens Returns {tablename: {columnname: reflected-column-dict}}. Reflection happens
while the connection is open, so we materialize the dicts before it closes. while the connection is open, so we materialize the dicts before it closes.
""" """
spec = importlib.util.spec_from_file_location('geenforce_baseline', migrations = []
str(_BASELINE)) for path in sorted(_VERSIONS.glob('[0-9]*.py')):
migration = importlib.util.module_from_spec(spec) spec = importlib.util.spec_from_file_location(
spec.loader.exec_module(migration) 'geenforce_migration_' + path.stem, str(path))
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
migrations.append(module)
engine = create_engine('sqlite://') engine = create_engine('sqlite://')
schema = {} schema = {}
@@ -69,7 +77,8 @@ def _migration_schema():
operations = Operations(MigrationContext.configure(connection)) operations = Operations(MigrationContext.configure(connection))
operations._install_proxy() operations._install_proxy()
try: try:
migration.upgrade() for migration in migrations:
migration.upgrade()
finally: finally:
operations._remove_proxy() operations._remove_proxy()
connection.commit() connection.commit()

View File

@@ -193,3 +193,37 @@ def test_backup_state_prefers_the_newest_confirmed_revision(db):
assert facts['wjpc01']['backupkind'] == 'udc' assert facts['wjpc01']['backupkind'] == 'udc'
assert facts['wjpc01']['backuplastseen'] is not None assert facts['wjpc01']['backuplastseen'] is not None
# -- reported display subtype -------------------------------------------------
def test_report_stores_the_subtype_the_device_reports(db):
"""A kiosk knows whether it is a Dashboard or a Lobby; it now says so.
Previously the fleet table inferred this from the DashboardDefault fqdn
mapping, which is empty unless somebody added a row per kiosk, so the
column was blank for every display.
"""
from plugins.geenforce.service import record_enforcement_report
record_enforcement_report({'hostname': 'WJKIOSK01', 'scopename': 'gea-shopfloor-display',
'subtype': 'Lobby', 'counts': {}})
db.session.flush()
stored = ManifestEnforcementReport.query.filter_by(
hostname='WJKIOSK01', iscurrent=True).one()
assert stored.subtype == 'Lobby'
def test_report_without_a_subtype_stores_none(db):
"""Every non-display PC type has no display-type.txt, so it reports no
subtype - that must land as NULL, not as an empty string."""
from plugins.geenforce.service import record_enforcement_report
record_enforcement_report({'hostname': 'WJPC77', 'scopename': 'common',
'subtype': '', 'counts': {}})
db.session.flush()
stored = ManifestEnforcementReport.query.filter_by(
hostname='WJPC77', iscurrent=True).one()
assert stored.subtype is None