Track manifest apps in the Applications catalog; make CMM gate contextual
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:
@@ -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('')
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
54
tests/test_plugins/test_geenforce_seed_apps.py
Normal file
54
tests/test_plugins/test_geenforce_seed_apps.py
Normal 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
|
||||||
Reference in New Issue
Block a user