Files
shopdb-flask/plugins/geenforce/importer.py
cproudlock d157502d5b Add GE-Enforce P2 admin CRUD API: scopes, entries, reorder, simulate, publish
Full HTTP admin surface behind the manifest editor (geenforce.manage for edits,
geenforce.publish for shipping):

- Scopes: POST/GET/PUT/DELETE /scopes[/<id>] (create imaging PC types, edit the
  ComputerType/MeasuringToolType mapping + metadata, delete).
- Entries: POST /scopes/<id>/entries, PUT/DELETE /entries/<id>. Payloads use the
  manifest Applications[] shape; populate_entry (refactored out of build_entry)
  updates an entry in place, resetting omitted fields and replacing children.
- Reorder: PUT /scopes/<id>/entries/reorder enforces the ordering contract
  (body must list exactly the scope's entry ids).
- Simulate: GET /scopes/<id>/simulate?pctype&subtype&hostname&machinenumber&
  cmmversion returns which entries apply and which filter excluded the rest,
  reusing the engine-mirror filters. The "what would this PC get" tool.
- Publish lifecycle: POST /scopes/<id>/publish (records publishedby from JWT),
  GET /scopes/<id>/versions, GET .../versions/<n> (frozen manifest),
  POST /scopes/<id>/rollback.

Entry type validated against ENTRY_TYPES; 8 CRUD tests. JWT+permission gated so
the authz sweep covers them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 17:13:27 -04:00

118 lines
4.9 KiB
Python

"""Parse on-share manifest.json / preinstall.json into draft DB rows.
The reverse of serializer.py. `build_scope` turns a parsed manifest dict into a
ManifestScope with its ordered entries and child rows (not yet committed).
`discover_share` walks a GE-Enforce share root and yields (scopename, phase,
manifest_dict) for common + every gea-shopfloor-* runtime manifest, skipping
`.bak` variants. `load_preinstall` reads a preinstall.json.
Import is a rebuild: the caller replaces a scope's draft rows so re-running is
idempotent.
"""
import json
import os
from .models import (
ManifestScope, ManifestEntry, ManifestEntryPcType, ManifestEntryHostname,
ManifestEntryMachineNumber, ManifestInUseCheck, ManifestInUseCheckProcess,
)
from .serializer import _SCALAR_FIELDS, _BOOL_FLAGS
# manifest key -> model attr (reverse of the serializer's scalar map).
_KEY_TO_ATTR = {mk: attr for attr, mk in _SCALAR_FIELDS}
_FLAG_TO_ATTR = {mk: attr for attr, mk in _BOOL_FLAGS}
# Scalar model attributes that a populate resets (so an update clears fields the
# new payload omits). name/entrytype are required and set explicitly.
_RESETTABLE_ATTRS = [attr for _, attr in _SCALAR_FIELDS
if attr not in ('name', 'entrytype')] + ['comment', 'regvalue']
def populate_entry(entry, entry_dict):
"""Set every field + child on an existing (or new) ManifestEntry from one
Applications[] entry dict. Existing children are replaced. Returns entry."""
entry.name = entry_dict.get('Name')
entry.entrytype = entry_dict.get('Type')
entry.comment = entry_dict.get('_comment')
for attr in _RESETTABLE_ATTRS:
if attr in ('comment', 'regvalue'):
continue
setattr(entry, attr, None)
for key, attr in _KEY_TO_ATTR.items():
if attr in ('name', 'entrytype'):
continue
if key in entry_dict and entry_dict[key] is not None:
setattr(entry, attr, entry_dict[key])
# RegValue stored as its raw JSON literal so DWord vs string typing survives.
entry.regvalue = (json.dumps(entry_dict['RegValue'])
if 'RegValue' in entry_dict else None)
for key, attr in _FLAG_TO_ATTR.items():
setattr(entry, attr, bool(entry_dict.get(key)))
# Multi-value filters -> child rows (replace, preserve order).
entry.pctypes = [ManifestEntryPcType(sortorder=i, pctypevalue=value)
for i, value in enumerate(entry_dict.get('PCTypes') or [])]
entry.hostnames = [ManifestEntryHostname(sortorder=i, hostnamepattern=value)
for i, value in enumerate(entry_dict.get('TargetHostnames') or [])]
entry.machinenumbers = [
ManifestEntryMachineNumber(sortorder=i, machinenumber=str(value))
for i, value in enumerate(entry_dict.get('TargetMachineNumbers') or [])]
# InUseCheck object + Processes[].
inuse = entry_dict.get('InUseCheck')
if inuse:
check = ManifestInUseCheck(behavior=inuse.get('Behavior'))
for i, proc in enumerate(inuse.get('Processes') or []):
check.processes.append(ManifestInUseCheckProcess(
sortorder=i,
processname=proc.get('Name'),
exepath=proc.get('ExePath'),
gracefulclosetimeoutsec=proc.get('GracefulCloseTimeoutSec')))
entry.inusecheck = check
else:
entry.inusecheck = None
return entry
def build_entry(entry_dict, sortorder):
"""Build a new ManifestEntry (+ children) from one Applications[] entry."""
entry = ManifestEntry(sortorder=sortorder)
return populate_entry(entry, entry_dict)
def build_scope(scopename, phase, manifest_dict, iscommon=False):
"""Build a ManifestScope (+ entries) from a parsed manifest dict."""
scope = ManifestScope(
scopename=scopename,
phase=phase,
manifestversion=str(manifest_dict.get('Version', '1.0')),
topcomment=manifest_dict.get('_comment'),
site=manifest_dict.get('Site'),
iscommon=iscommon)
for i, entry_dict in enumerate(manifest_dict.get('Applications') or []):
scope.entries.append(build_entry(entry_dict, i))
return scope
def discover_share(shareroot):
"""Yield (scopename, phase, manifest_dict) for runtime manifests on a share.
Reads `common/manifest.json` and every `gea-shopfloor-*/manifest.json`.
Skips `.bak` / `.pre-*.bak` variants (only the exact `manifest.json`).
"""
for name in sorted(os.listdir(shareroot)):
if name != 'common' and not name.startswith('gea-shopfloor-'):
continue
path = os.path.join(shareroot, name, 'manifest.json')
if not os.path.isfile(path):
continue
with open(path) as handle:
manifest = json.load(handle)
yield name, 'runtime', manifest
def load_manifest_file(path):
"""Parse a single manifest.json / preinstall.json file."""
with open(path) as handle:
return json.load(handle)