Track manifest apps in the Applications catalog; make CMM gate contextual
All checks were successful
CI / backend (push) Successful in 1m43s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 8s

seed-applications: a flask geenforce seed-applications command + service that
reads the imaging-PC-type manifests and creates a core Application for every
installer entry (MSI/EXE/CMD/BAT), so shopdb tracks what GE-Enforce actually
deploys. Idempotent, deduped by appname; File/Registry/PS1/INF config entries
are skipped. Run against the West Jefferson reference: 27 apps tracked (PC-DMIS
2016/2019/2026, eDNC, Oracle Client, Adobe Reader, HostExplorer, the VC++ redist
matrix, Keyence VR-6000, PowerShell, Display Kiosk, ...). 2 tests.

Editor: the CMM version gate (_CmmVersion) now only shows for CMM scopes - it is
metrology-specific, so a printer/common entry form no longer carries the
irrelevant field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-12 21:21:49 -04:00
parent d9d40297b6
commit 796007bcca
4 changed files with 119 additions and 2 deletions

View File

@@ -214,7 +214,7 @@
<label>PC types (comma)<input v-model="entryForm.PCTypes" placeholder="* or gea-shopfloor-cmm,..." /></label> <label>PC types (comma)<input v-model="entryForm.PCTypes" placeholder="* or gea-shopfloor-cmm,..." /></label>
<label>Target hostnames (comma)<input v-model="entryForm.TargetHostnames" /></label> <label>Target hostnames (comma)<input v-model="entryForm.TargetHostnames" /></label>
<label>Target machine numbers (comma)<input v-model="entryForm.TargetMachineNumbers" /></label> <label>Target machine numbers (comma)<input v-model="entryForm.TargetMachineNumbers" /></label>
<label>CMM version gate<input v-model="entryForm._CmmVersion" placeholder="2016 / 2019 / 2026" /></label> <label v-if="isCmmScope">CMM version gate<input v-model="entryForm._CmmVersion" placeholder="2016 / 2019 / 2026" /></label>
<label>Wait timeout (sec)<input type="number" v-model.number="entryForm.WaitTimeoutSec" /></label> <label>Wait timeout (sec)<input type="number" v-model.number="entryForm.WaitTimeoutSec" /></label>
<label>Log file<input v-model="entryForm.LogFile" /></label> <label>Log file<input v-model="entryForm.LogFile" /></label>
@@ -273,7 +273,7 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref, computed } from 'vue'
import api from '../../api' import api from '../../api'
const ENTRY_TYPES = ['MSI', 'EXE', 'CMD', 'BAT', 'PS1', 'INF', 'File', 'Registry'] const ENTRY_TYPES = ['MSI', 'EXE', 'CMD', 'BAT', 'PS1', 'INF', 'File', 'Registry']
@@ -285,6 +285,9 @@ const INUSE_BEHAVIORS = ['Defer', 'CloseAndReopen', 'ForceClose', 'ScheduleForRe
const scopes = ref([]) const scopes = ref([])
const selectedId = ref(null) const selectedId = ref(null)
const detail = ref(null) const detail = ref(null)
// The CMM version gate (_CmmVersion) only applies to CMM scopes; hide it
// elsewhere so a printer/common entry form is not cluttered with an irrelevant field.
const isCmmScope = computed(() => /cmm/i.test(detail.value?.scopename || ''))
const error = ref('') const error = ref('')
const notice = ref('') const notice = ref('')
const shareRoot = ref('') const shareRoot = ref('')

View File

@@ -178,6 +178,27 @@ class GeEnforcePlugin(BasePlugin):
db.session.commit() db.session.commit()
click.echo(f"Published {scopename}/{phase} as v{version}.") click.echo(f"Published {scopename}/{phase} as v{version}.")
@geenforce_cli.command('seed-applications')
@click.option('--shareroot', required=True)
@click.option('--preinstall', default=None)
def seed_applications_cmd(shareroot, preinstall):
"""Track the apps the manifests install in the core Applications catalog."""
from flask import current_app
from .importer import discover_share, load_manifest_file
from .service import seed_applications_from_manifests
with current_app.app_context():
sources = list(discover_share(shareroot))
if preinstall:
sources.append(('preinstall', 'preinstall',
load_manifest_file(preinstall)))
result = seed_applications_from_manifests(sources)
db.session.commit()
click.echo(f"Applications: {len(result['created'])} created, "
f"{len(result['existing'])} already tracked.")
for name in result['created']:
click.echo(f" + {name}")
@geenforce_cli.command('export-share') @geenforce_cli.command('export-share')
@click.argument('scopename') @click.argument('scopename')
@click.option('--shareroot', required=True) @click.option('--shareroot', required=True)

View File

@@ -94,6 +94,45 @@ def rollback_scope(scopename, phase, versionnumber):
return versionnumber return versionnumber
# Entry types that install actual applications (vs File/Registry/PS1/INF config).
INSTALLER_TYPES = {'MSI', 'EXE', 'CMD', 'BAT'}
def seed_applications_from_manifests(manifests):
"""Populate the core Applications catalog from what the manifests install.
For every installer entry (MSI/EXE/CMD/BAT) across the given manifests,
create an Application (idempotent, deduped by appname) so shopdb tracks the
apps GE-Enforce deploys. `manifests` is a list of (scopename, phase, dict).
Returns {'created': [...], 'existing': [...]} (uncommitted).
"""
from shopdb.api import Application
created, existing, seen = [], [], set()
for _scopename, _phase, manifest in manifests:
for entry in manifest.get('Applications', []):
if entry.get('Type') not in INSTALLER_TYPES:
continue
name = (entry.get('Name') or '').strip()
if not name or name.lower() in seen:
continue
seen.add(name.lower())
if Application.query.filter(Application.appname.ilike(name)).first():
existing.append(name)
continue
description = None
comment = entry.get('_comment')
if comment:
description = comment.split('.')[0].strip()[:255]
db.session.add(Application(
appname=name[:100],
appdescription=description,
installpath=(entry.get('Installer') or '')[:255] or None,
isinstallable=True))
created.append(name)
return {'created': created, 'existing': existing}
def record_enforcement_report(payload): def record_enforcement_report(payload):
"""Record one PC's enforcement cycle (observed state). Upserts the latest """Record one PC's enforcement cycle (observed state). Upserts the latest
report per (hostname, scopename, phase) and keeps prior ones as history. report per (hostname, scopename, phase) and keeps prior ones as history.

View File

@@ -0,0 +1,54 @@
"""GE-Enforce seeds the core Applications catalog from what the manifests install.
Only installer entries (MSI/EXE/CMD/BAT) become Applications; File/Registry/PS1/
INF config entries do not. Idempotent + deduped by appname.
"""
from plugins.geenforce import service
from shopdb.core.models import Application
MANIFEST = {
'Version': '2.6',
'Applications': [
{'Name': 'eDNC', 'Type': 'MSI', 'Installer': 'apps/edns.msi',
'_comment': 'eDNC (Universal Data Collection). Bundles NTLARS.'},
{'Name': 'UDC', 'Type': 'EXE', 'Installer': 'apps/udc.exe'},
{'Name': 'Config drop', 'Type': 'File', 'Source': 'configs/x.json',
'Destination': 'C:\\x.json'},
{'Name': 'Firewall rule', 'Type': 'PS1', 'Script': 'scripts/fw.ps1'},
{'Name': 'FMS host pin', 'Type': 'Registry', 'RegPath': 'HKLM:\\x',
'RegName': 'y', 'RegValue': 1, 'RegType': 'DWord'},
],
}
def test_seed_only_installer_apps(db):
result = service.seed_applications_from_manifests(
[('gea-shopfloor-collections', 'runtime', MANIFEST)])
service.db.session.commit()
assert set(result['created']) == {'eDNC', 'UDC'}
names = {a.appname for a in Application.query.all()}
assert 'eDNC' in names and 'UDC' in names
# config / script / registry entries are not applications
assert 'Config drop' not in names
assert 'Firewall rule' not in names
assert 'FMS host pin' not in names
edns = Application.query.filter_by(appname='eDNC').first()
assert edns.isinstallable is True
assert edns.installpath == 'apps/edns.msi'
assert edns.appdescription.startswith('eDNC (Universal Data Collection)')
def test_seed_is_idempotent_and_deduped(db):
manifests = [('a', 'runtime', MANIFEST), ('b', 'runtime', MANIFEST)]
first = service.seed_applications_from_manifests(manifests)
service.db.session.commit()
# deduped across the two manifests: 2 created, not 4
assert len(first['created']) == 2
# re-run: nothing new, both already tracked
second = service.seed_applications_from_manifests(manifests)
service.db.session.commit()
assert second['created'] == []
assert set(second['existing']) == {'eDNC', 'UDC'}
assert Application.query.filter(Application.appname.in_(['eDNC', 'UDC'])).count() == 2