Initial commit: enterprise digital platform with portal SSO, DigiServer, IT Assets, NetworkView, Server Monitor

This commit is contained in:
ske087
2026-05-10 21:07:50 +03:00
commit 8d9df56b0b
364 changed files with 73655 additions and 0 deletions
+197
View File
@@ -0,0 +1,197 @@
"""
module_manager.py — start/stop/status helpers for EDP sub-services.
Used exclusively in development mode (start-dev.sh launcher). Each service
writes its PID to .dev-logs/<name>.pid so the portal can signal it.
Module state (enabled/disabled) is persisted in the portal's SQLite DB
(ModuleConfig table) and mirrored to .dev-module-state.json so that
start-dev.sh can respect it on the next launch.
"""
import json
import logging
import os
import signal
import subprocess
from typing import Optional
log = logging.getLogger(__name__)
# ── Directory helpers ─────────────────────────────────────────────────────────
def _root() -> str:
"""Return the Enterprise_digital-platform root directory at runtime."""
# This file lives at: portal/app/services/module_manager.py
# → services/ → app/ → portal/ → ROOT
return os.path.dirname(
os.path.dirname(
os.path.dirname(
os.path.dirname(os.path.abspath(__file__))
)
)
)
def _log_dir() -> str:
return os.path.join(_root(), '.dev-logs')
def _pid_path(app_id: str) -> str:
return os.path.join(_log_dir(), f'{app_id}.pid')
def _state_path() -> str:
return os.path.join(_root(), '.dev-module-state.json')
# ── PID file helpers ──────────────────────────────────────────────────────────
def _read_pid(app_id: str) -> Optional[int]:
path = _pid_path(app_id)
if not os.path.exists(path):
return None
try:
with open(path) as fp:
return int(fp.read().strip())
except (ValueError, OSError):
return None
def _write_pid(app_id: str, pid: int) -> None:
os.makedirs(_log_dir(), exist_ok=True)
with open(_pid_path(app_id), 'w') as fp:
fp.write(str(pid))
def _clear_pid(app_id: str) -> None:
path = _pid_path(app_id)
try:
os.unlink(path)
except OSError:
pass
# ── Module state file ─────────────────────────────────────────────────────────
def write_module_state(app_id: str, enabled: bool) -> None:
"""Mirror enabled/disabled state to .dev-module-state.json for start-dev.sh."""
path = _state_path()
state: dict = {}
if os.path.exists(path):
try:
with open(path) as fp:
state = json.load(fp)
except (json.JSONDecodeError, OSError):
state = {}
state[app_id] = enabled
with open(path, 'w') as fp:
json.dump(state, fp, indent=2)
# ── Public API ────────────────────────────────────────────────────────────────
def is_running(app_id: str) -> bool:
"""Return True if the service process is alive."""
pid = _read_pid(app_id)
if pid is None:
return False
try:
os.kill(pid, 0)
return True
except ProcessLookupError:
_clear_pid(app_id)
return False
except PermissionError:
return True # process exists, owned by another uid
def stop_service(app_id: str) -> bool:
"""Send SIGTERM to the service. Returns True if signal was delivered."""
pid = _read_pid(app_id)
if pid is None:
return False
try:
os.kill(pid, signal.SIGTERM)
_clear_pid(app_id)
log.info('Sent SIGTERM to %s (PID %d)', app_id, pid)
return True
except ProcessLookupError:
_clear_pid(app_id)
return False
def start_service(app_id: str) -> bool:
"""Spawn the service as a detached background process. Returns True on success."""
root = _root()
spec = _get_spec(app_id, root)
if spec is None:
log.warning('No service spec for app_id=%s', app_id)
return False
log_file = os.path.join(_log_dir(), f'{app_id}.log')
os.makedirs(_log_dir(), exist_ok=True)
env = {**os.environ, **spec.get('env', {})}
with open(log_file, 'a') as lf:
proc = subprocess.Popen(
spec['cmd'],
env=env,
cwd=spec.get('cwd'),
stdout=lf,
stderr=lf,
start_new_session=True, # detach from the Flask process group
)
_write_pid(app_id, proc.pid)
log.info('Started %s as PID %d', app_id, proc.pid)
return True
# ── Service command specs ─────────────────────────────────────────────────────
def _get_spec(app_id: str, root: str) -> Optional[dict]:
"""Return the launch spec for a given app_id, or None if unknown."""
jwt = os.environ.get('PORTAL_JWT_SECRET', 'change-this-jwt-secret-in-production')
specs: dict = {
'digiserver': {
'cmd': [
f'{root}/digiserver-v2/.venv/bin/gunicorn',
'--bind', '0.0.0.0:5002',
'--workers', '2',
'--timeout', '120',
'--chdir', f'{root}/digiserver-v2',
'wsgi:application',
],
'env': {
'FLASK_ENV': 'development',
'ADMIN_USERNAME': os.environ.get('ADMIN_USERNAME', 'admin'),
'ADMIN_PASSWORD': os.environ.get('ADMIN_PASSWORD', 'admin123'),
'PORTAL_JWT_SECRET': jwt,
},
'cwd': f'{root}/digiserver-v2',
},
'itassets': {
'cmd': [
f'{root}/IT_asset_management/.venv/bin/python',
f'{root}/IT_asset_management/run.py',
],
'env': {
'FLASK_ENV': 'development',
'SQLALCHEMY_DATABASE_URI': f'sqlite:///{root}/IT_asset_management/data/itassets.db',
'PORTAL_JWT_SECRET': jwt,
'FLASK_APP': 'run.py',
},
'cwd': f'{root}/IT_asset_management',
},
'networkview': {
'cmd': ['node', f'{root}/NetworkView/backend/src/index.js'],
'env': {
'PORT': '3001',
'PORTAL_JWT_SECRET': jwt,
'NODE_ENV': 'development',
'DB_PATH': f'{root}/NetworkView/data/networkview.db',
},
'cwd': f'{root}/NetworkView/backend',
},
}
return specs.get(app_id)