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>
118 lines
4.9 KiB
Python
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)
|