geenforce: let a kiosk say what it is instead of guessing
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 never told shopdb, so the fleet table inferred it from the DashboardDefault fqdn mapping, which is empty unless somebody added a row per kiosk. The column was blank for every display. The client now reads that file and reports it, the report stores it, and the API prefers the reported value with the old mapping left as a fallback for hosts still on an older client. Reported by the device beats inferred from a lookup table, the same way enforcerversion already works. A PC with no display-type.txt reports nothing rather than something invented, and an empty string lands as NULL. Two guards had to learn about it. The DDL parity check read only the 0001 baseline, so a column added by a later revision looked like drift even though its migration existed; it now runs the whole chain, which is what 'do the models match what the migrations build' means. 0002 added a whole table rather than a column, which is why this is the first time it bit.
This commit is contained in:
@@ -1106,7 +1106,9 @@ def list_reports():
|
||||
'machineassetid': known.get('machineassetid'),
|
||||
'toolassetnumber': known.get('toolassetnumber'),
|
||||
'toolassetid': known.get('toolassetid'),
|
||||
'displayrole': known.get('displayrole'),
|
||||
# Reported by the device wins; the DashboardDefault mapping is a
|
||||
# fallback for hosts on an older client that does not send it.
|
||||
'displayrole': report.subtype or known.get('displayrole'),
|
||||
'backupkind': known.get('backupkind'),
|
||||
'backuplastseen': known.get('backuplastseen'),
|
||||
'scopename': report.scopename,
|
||||
|
||||
@@ -210,7 +210,8 @@ function New-ShopdbReport {
|
||||
param([string]$Hostname = $env:COMPUTERNAME,
|
||||
[Parameter(Mandatory)][string]$Scope,
|
||||
[int]$AppliedVersion,
|
||||
[Parameter(Mandatory)][hashtable]$Summary)
|
||||
[Parameter(Mandatory)][hashtable]$Summary,
|
||||
[string]$SubType = (Get-ShopdbSubType))
|
||||
$results = @(foreach ($entry in @($Summary.Results)) {
|
||||
if ($null -eq $entry) { continue }
|
||||
$mapped = @{
|
||||
@@ -230,6 +231,7 @@ function New-ShopdbReport {
|
||||
scopename = $Scope
|
||||
appliedversion = $AppliedVersion
|
||||
enforcerversion = $Summary.EnforcerVersion
|
||||
subtype = $SubType
|
||||
counts = @{
|
||||
installed = [int]$Summary.Installed
|
||||
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 {
|
||||
<#
|
||||
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, `
|
||||
Compare-ShopdbShadow, Send-ShopdbReport, New-ShopdbReport, Read-CachedVersion, `
|
||||
Get-ShopdbPayload, Resolve-ShopdbPayloads, Merge-ShopdbManifests, `
|
||||
ConvertTo-ShopdbSummary
|
||||
ConvertTo-ShopdbSummary, Get-ShopdbSubType
|
||||
|
||||
28
plugins/geenforce/migrations/versions/0003_report_subtype.py
Normal file
28
plugins/geenforce/migrations/versions/0003_report_subtype.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""Record the display subtype a PC reports for itself.
|
||||
|
||||
A kiosk knows whether it is a Dashboard, Lobby or 3DPrintRoom - the dispatcher
|
||||
reads C:\\Enrollment\\display-type.txt to pick its target - but it never told
|
||||
ShopDB, so the reports table had to infer it from the DashboardDefault
|
||||
fqdn/ip mapping, which is blank unless someone added a row per kiosk. Reported
|
||||
by the device beats inferred from a mapping table, the same way enforcerversion
|
||||
already works.
|
||||
|
||||
Revision ID: geenforce0003subtype
|
||||
Revises: geenforce0002blobs
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = 'geenforce0003subtype'
|
||||
down_revision = 'geenforce0002blobs'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('manifestenforcementreports',
|
||||
sa.Column('subtype', sa.String(length=50), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('manifestenforcementreports', 'subtype')
|
||||
@@ -295,6 +295,11 @@ class ManifestEnforcementReport(db.Model):
|
||||
# published version to see whether this PC received the latest manifest.
|
||||
appliedversion = db.Column(db.Integer, nullable=True)
|
||||
enforcerversion = db.Column(db.String(20), nullable=True)
|
||||
# What the PC says it IS within its scope (a display's Dashboard / Lobby /
|
||||
# 3DPrintRoom). Reported by the device, which reads it from its own
|
||||
# enrollment file - not inferred from a mapping table that may have no row
|
||||
# for this host.
|
||||
subtype = db.Column(db.String(50), nullable=True)
|
||||
installedcount = db.Column(db.Integer, nullable=False, default=0)
|
||||
skippedcount = db.Column(db.Integer, nullable=False, default=0)
|
||||
failedcount = db.Column(db.Integer, nullable=False, default=0)
|
||||
|
||||
@@ -263,6 +263,7 @@ def record_enforcement_report(payload):
|
||||
phase=phase,
|
||||
appliedversion=payload.get('appliedversion'),
|
||||
enforcerversion=payload.get('enforcerversion'),
|
||||
subtype=(payload.get('subtype') or None),
|
||||
installedcount=installed,
|
||||
skippedcount=int(counts.get('skipped', 0)),
|
||||
failedcount=failed,
|
||||
|
||||
@@ -52,7 +52,7 @@ EXPECTED_HEAD_REVISION['measuringtools'] = 'measuringtools0001baseline'
|
||||
EXPECTED_HEAD_REVISION['backups'] = 'backups0003clearlastseen'
|
||||
# geenforce adds the content-addressed blob store (manifestblobs) on top of its
|
||||
# baseline.
|
||||
EXPECTED_HEAD_REVISION['geenforce'] = 'geenforce0002blobs'
|
||||
EXPECTED_HEAD_REVISION['geenforce'] = 'geenforce0003subtype'
|
||||
# printers adds the printersupplyalerts crossing-state table on top of its anchor.
|
||||
EXPECTED_HEAD_REVISION['printers'] = 'printers0002supplyalerts'
|
||||
# machines (renamed from equipment) keeps its original anchor id and adds the
|
||||
|
||||
@@ -22,9 +22,9 @@ from alembic.migration import MigrationContext
|
||||
from alembic.operations import Operations
|
||||
|
||||
|
||||
_BASELINE = (Path(__file__).resolve().parent.parent.parent / 'plugins' /
|
||||
'geenforce' / 'migrations' / 'versions' /
|
||||
'0001_geenforce_baseline.py')
|
||||
_VERSIONS = (Path(__file__).resolve().parent.parent.parent / 'plugins' /
|
||||
'geenforce' / 'migrations' / 'versions')
|
||||
_BASELINE = _VERSIONS / '0001_geenforce_baseline.py'
|
||||
|
||||
|
||||
def _geenforce_models():
|
||||
@@ -53,15 +53,23 @@ def _model_schema(db, models):
|
||||
|
||||
|
||||
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
|
||||
while the connection is open, so we materialize the dicts before it closes.
|
||||
"""
|
||||
spec = importlib.util.spec_from_file_location('geenforce_baseline',
|
||||
str(_BASELINE))
|
||||
migration = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(migration)
|
||||
migrations = []
|
||||
for path in sorted(_VERSIONS.glob('[0-9]*.py')):
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
'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://')
|
||||
schema = {}
|
||||
@@ -69,7 +77,8 @@ def _migration_schema():
|
||||
operations = Operations(MigrationContext.configure(connection))
|
||||
operations._install_proxy()
|
||||
try:
|
||||
migration.upgrade()
|
||||
for migration in migrations:
|
||||
migration.upgrade()
|
||||
finally:
|
||||
operations._remove_proxy()
|
||||
connection.commit()
|
||||
|
||||
@@ -193,3 +193,37 @@ def test_backup_state_prefers_the_newest_confirmed_revision(db):
|
||||
|
||||
assert facts['wjpc01']['backupkind'] == 'udc'
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user