Initial import: shopdb-flask multi-site asset platform
Flask + Vue rewrite of the classic-ASP shopdb, built as a plugin framework so sister GE Aerospace sites can adopt it. Product version 0.7.0-pre-0.8.0, plugin contract 0.11.0, 966 backend tests. Key entry points: - docs/INSTALL-WINDOWS-IIS.md - tested Windows + IIS install runbook (own-site and subpath deployment methods) - docs/PILOT-DEPLOY.md - production pilot: stand up + import + verify - docs/IMPORT-API.md - schema-agnostic legacy data import contract - scripts/site_imports/wjf/ - West Jefferson reference import loader - docs/adr/ - twelve accepted architecture decision records
This commit is contained in:
194
plugins/geenforce/plugin.py
Normal file
194
plugins/geenforce/plugin.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""GE-Enforce manifest-store plugin.
|
||||
|
||||
Owns the imaging-PC-type scopes and their install manifests as shopdb data
|
||||
(see the ge-enforce-plugin proposal in the project wiki). This is the P0/P1 foundation: models,
|
||||
the alias-graph seed, and the client-facing manifest endpoint. Authoring UI and
|
||||
client cutover come in later phases.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional, Type
|
||||
|
||||
import click
|
||||
from flask import Flask, Blueprint
|
||||
|
||||
from shopdb.plugins.base import BasePlugin, PluginMeta
|
||||
from shopdb.api import db
|
||||
|
||||
from .api import geenforce_bp
|
||||
from .models import (
|
||||
ManifestScope, ManifestEntry, ManifestEntryPcType, ManifestEntryHostname,
|
||||
ManifestEntryMachineNumber, ManifestInUseCheck, ManifestInUseCheckProcess,
|
||||
ManifestPublishedVersion, ManifestPayload, ManifestEnforcementReport,
|
||||
ManifestEnforcementResult, PcTypeAlias,
|
||||
)
|
||||
from .filters import ALIAS_GROUPS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GeEnforcePlugin(BasePlugin):
|
||||
"""Desired-state manifest store for the GE-Enforce client."""
|
||||
|
||||
def __init__(self):
|
||||
self._manifest = self._load_manifest()
|
||||
|
||||
def _load_manifest(self) -> Dict:
|
||||
manifest_path = Path(__file__).parent / 'manifest.json'
|
||||
if manifest_path.exists():
|
||||
with open(manifest_path, 'r') as handle:
|
||||
return json.load(handle)
|
||||
return {}
|
||||
|
||||
@property
|
||||
def meta(self) -> PluginMeta:
|
||||
return PluginMeta(
|
||||
name=self._manifest.get('name', 'geenforce'),
|
||||
version=self._manifest.get('version', '0.1.0'),
|
||||
description=self._manifest.get('description', 'GE-Enforce manifest store'),
|
||||
author=self._manifest.get('author', 'ShopDB Team'),
|
||||
dependencies=self._manifest.get('dependencies', []),
|
||||
core_version=self._manifest.get('core_version', '>=0.7.0,<1.0.0'),
|
||||
api_prefix=self._manifest.get('api_prefix', '/api/geenforce'),
|
||||
)
|
||||
|
||||
def get_blueprint(self) -> Optional[Blueprint]:
|
||||
return geenforce_bp
|
||||
|
||||
def get_models(self) -> List[Type]:
|
||||
return [
|
||||
ManifestScope, ManifestEntry, ManifestEntryPcType,
|
||||
ManifestEntryHostname, ManifestEntryMachineNumber,
|
||||
ManifestInUseCheck, ManifestInUseCheckProcess,
|
||||
ManifestPublishedVersion, ManifestPayload,
|
||||
ManifestEnforcementReport, ManifestEnforcementResult, PcTypeAlias,
|
||||
]
|
||||
|
||||
def get_permissions(self) -> List:
|
||||
"""RBAC permissions this plugin owns (edit vs ship are split)."""
|
||||
return [
|
||||
('geenforce.manage', 'Edit imaging PC types and manifest drafts',
|
||||
'geenforce'),
|
||||
('geenforce.publish', 'Publish, roll back, and export manifests',
|
||||
'geenforce'),
|
||||
('geenforce.fetch', 'Fetch published manifests (client service token)',
|
||||
'geenforce'),
|
||||
('geenforce.report', 'Report enforcement results (client service token)',
|
||||
'geenforce'),
|
||||
]
|
||||
|
||||
def get_navigation_items(self) -> List[Dict]:
|
||||
"""Top-level sidebar section (GE-Enforce is a large operational surface,
|
||||
not a mere setting). The tabbed shell hosts Manifests + Reports."""
|
||||
return [
|
||||
{
|
||||
'name': 'GE-Enforce',
|
||||
'icon': 'shield',
|
||||
'route': '/geenforce/manifests',
|
||||
'position': 46,
|
||||
},
|
||||
]
|
||||
|
||||
def init_app(self, app: Flask, db_instance) -> None:
|
||||
logger.info(f"GE-Enforce plugin initialized (v{self.meta.version})")
|
||||
|
||||
def on_install(self, app: Flask) -> None:
|
||||
with app.app_context():
|
||||
self._seed_aliases()
|
||||
db.session.commit()
|
||||
logger.info("GE-Enforce plugin installed")
|
||||
|
||||
def _seed_aliases(self) -> None:
|
||||
"""Seed pctypealiases from the engine lib's alias graph (idempotent)."""
|
||||
for group_index, group in enumerate(ALIAS_GROUPS):
|
||||
for aliasname in group:
|
||||
exists = PcTypeAlias.query.filter_by(
|
||||
aliasgroup=group_index, aliasname=aliasname).first()
|
||||
if not exists:
|
||||
db.session.add(PcTypeAlias(
|
||||
aliasgroup=group_index, aliasname=aliasname))
|
||||
|
||||
def get_cli_commands(self) -> List:
|
||||
"""CLI: parity (Gate A), import-share, export-share, publish."""
|
||||
|
||||
@click.group('geenforce')
|
||||
def geenforce_cli():
|
||||
"""GE-Enforce manifest-store commands."""
|
||||
|
||||
@geenforce_cli.command('parity')
|
||||
@click.option('--shareroot', required=True,
|
||||
help='GE-Enforce share root (contains common/, '
|
||||
'gea-shopfloor-*/).')
|
||||
@click.option('--preinstall', default=None,
|
||||
help='Optional path to a preinstall.json to include.')
|
||||
def parity_cmd(shareroot, preinstall):
|
||||
"""Prove import+export is behaviorally lossless (Gate A). DB-free."""
|
||||
from .importer import discover_share, load_manifest_file
|
||||
from .parity import run_parity, format_result
|
||||
|
||||
manifests = list(discover_share(shareroot))
|
||||
if preinstall:
|
||||
manifests.append(
|
||||
('preinstall', 'preinstall', load_manifest_file(preinstall)))
|
||||
results, ok = run_parity(manifests)
|
||||
for result in results:
|
||||
click.echo(format_result(result))
|
||||
click.echo(f"RESULT: {'PASS' if ok else 'FAIL'} "
|
||||
f"({len(results)} scopes)")
|
||||
raise SystemExit(0 if ok else 1)
|
||||
|
||||
@geenforce_cli.command('import-share')
|
||||
@click.option('--shareroot', required=True)
|
||||
@click.option('--preinstall', default=None)
|
||||
@click.option('--scope', default=None,
|
||||
help='Import only this scope name (else all).')
|
||||
def import_share_cmd(shareroot, preinstall, scope):
|
||||
"""Import on-share manifests into draft rows (idempotent rebuild)."""
|
||||
from flask import current_app
|
||||
from .importer import discover_share, load_manifest_file
|
||||
from .service import replace_scope_draft
|
||||
|
||||
with current_app.app_context():
|
||||
sources = list(discover_share(shareroot))
|
||||
if preinstall:
|
||||
sources.append(('preinstall', 'preinstall',
|
||||
load_manifest_file(preinstall)))
|
||||
count = 0
|
||||
for name, phase, manifest in sources:
|
||||
if scope and name != scope:
|
||||
continue
|
||||
replace_scope_draft(name, phase, manifest)
|
||||
count += 1
|
||||
db.session.commit()
|
||||
click.echo(f"Imported {count} scope(s).")
|
||||
|
||||
@geenforce_cli.command('publish')
|
||||
@click.argument('scopename')
|
||||
@click.option('--phase', default='runtime')
|
||||
@click.option('--notes', default=None)
|
||||
def publish_cmd(scopename, phase, notes):
|
||||
"""Freeze the current draft of a scope into a published snapshot."""
|
||||
from flask import current_app
|
||||
from .service import publish_scope
|
||||
|
||||
with current_app.app_context():
|
||||
version = publish_scope(scopename, phase, notes=notes)
|
||||
db.session.commit()
|
||||
click.echo(f"Published {scopename}/{phase} as v{version}.")
|
||||
|
||||
@geenforce_cli.command('export-share')
|
||||
@click.argument('scopename')
|
||||
@click.option('--shareroot', required=True)
|
||||
@click.option('--phase', default='runtime')
|
||||
def export_share_cmd(scopename, shareroot, phase):
|
||||
"""Write a scope's published JSON to the share (with history backup)."""
|
||||
from flask import current_app
|
||||
from .service import export_scope_to_share
|
||||
|
||||
with current_app.app_context():
|
||||
path = export_scope_to_share(scopename, phase, shareroot)
|
||||
click.echo(f"Exported to {path}.")
|
||||
|
||||
return [geenforce_cli]
|
||||
Reference in New Issue
Block a user