d48ef65b35
- New: services/sensu_db.py (SQLite, 5 tables) - New: services/project_service.py (project registry, port allocation, dep resolution) - Integrated into main.py (step 11.6) - Tests: 23/23 passing (15 original + 8 new)
120 lines
4.8 KiB
Python
120 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""SenSu SQLite 持久化层 — 替代零散 JSON 文件"""
|
|
import sqlite3, os, json, logging, threading
|
|
from typing import Optional, Dict, List, Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DEFAULT_DB_PATH = "data/sensu.db"
|
|
|
|
class SenSuDB:
|
|
def __init__(self, db_path: str = DEFAULT_DB_PATH):
|
|
os.makedirs(os.path.dirname(db_path) or ".", exist_ok=True)
|
|
self.db_path = db_path
|
|
self._local = threading.local()
|
|
self._init_schema()
|
|
|
|
@property
|
|
def conn(self):
|
|
if not hasattr(self._local, "conn") or self._local.conn is None:
|
|
self._local.conn = sqlite3.connect(self.db_path)
|
|
self._local.conn.row_factory = sqlite3.Row
|
|
self._local.conn.execute("PRAGMA journal_mode=WAL")
|
|
self._local.conn.execute("PRAGMA foreign_keys=ON")
|
|
return self._local.conn
|
|
|
|
def _init_schema(self):
|
|
c = self.conn
|
|
c.executescript("""
|
|
CREATE TABLE IF NOT EXISTS plugins (
|
|
name TEXT PRIMARY KEY,
|
|
version TEXT DEFAULT '0.1.0',
|
|
status TEXT DEFAULT 'unloaded',
|
|
config_json TEXT DEFAULT '{}',
|
|
permissions_json TEXT DEFAULT '[]',
|
|
depends_on TEXT DEFAULT '[]',
|
|
project_path TEXT,
|
|
project_port INTEGER,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
CREATE TABLE IF NOT EXISTS permissions (
|
|
plugin_name TEXT,
|
|
permission TEXT,
|
|
granted INTEGER DEFAULT 0,
|
|
granted_at TEXT,
|
|
PRIMARY KEY (plugin_name, permission)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS config_kv (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT,
|
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
CREATE TABLE IF NOT EXISTS audit_log (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
plugin_name TEXT,
|
|
action TEXT,
|
|
detail TEXT,
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
""")
|
|
c.commit()
|
|
|
|
# -- Plugin CRUD --
|
|
def save_plugin(self, name: str, **kwargs):
|
|
fields = ["name"] + list(kwargs.keys())
|
|
placeholders = ["?"] * len(fields)
|
|
values = [name] + list(kwargs.values())
|
|
for k in ["config", "permissions", "depends_on"]:
|
|
if k in kwargs and not isinstance(kwargs[k], str):
|
|
kwargs[k] = json.dumps(kwargs[k])
|
|
idx = fields.index(k)
|
|
values[idx] = kwargs[k]
|
|
sql = f"INSERT OR REPLACE INTO plugins ({','.join(fields)}) VALUES ({','.join(placeholders)})"
|
|
self.conn.execute("UPDATE plugins SET updated_at=datetime('now') WHERE name=?", [name])
|
|
self.conn.execute(sql, values)
|
|
self.conn.commit()
|
|
|
|
def get_plugin(self, name: str) -> Optional[Dict]:
|
|
row = self.conn.execute("SELECT * FROM plugins WHERE name=?", [name]).fetchone()
|
|
if not row: return None
|
|
d = dict(row)
|
|
for f in ["config_json", "permissions_json", "depends_on"]:
|
|
if d.get(f):
|
|
try: d[f.replace("_json","")] = json.loads(d.pop(f))
|
|
except: pass
|
|
return d
|
|
|
|
def list_plugins(self) -> List[Dict]:
|
|
return [dict(r) for r in self.conn.execute("SELECT name,version,status,project_path,project_port FROM plugins").fetchall()]
|
|
|
|
# -- Permissions --
|
|
def grant_permission(self, plugin: str, perm: str):
|
|
self.conn.execute(
|
|
"INSERT OR REPLACE INTO permissions(plugin_name,permission,granted,granted_at) VALUES(?,?,1,datetime('now'))",
|
|
[plugin, perm])
|
|
self.conn.commit()
|
|
|
|
def check_permission(self, plugin: str, perm: str) -> bool:
|
|
r = self.conn.execute("SELECT granted FROM permissions WHERE plugin_name=? AND permission=?", [plugin, perm]).fetchone()
|
|
return bool(r and r[0])
|
|
|
|
# -- Config --
|
|
def set_config(self, key: str, value: str):
|
|
self.conn.execute("INSERT OR REPLACE INTO config_kv(key,value,updated_at) VALUES(?,?,datetime('now'))", [key, value])
|
|
self.conn.commit()
|
|
|
|
def get_config(self, key: str, default=None) -> Optional[str]:
|
|
r = self.conn.execute("SELECT value FROM config_kv WHERE key=?", [key]).fetchone()
|
|
return r[0] if r else default
|
|
|
|
# -- Audit --
|
|
def log_audit(self, plugin: str, action: str, detail: str = ""):
|
|
self.conn.execute("INSERT INTO audit_log(plugin_name,action,detail) VALUES(?,?,?)", [plugin, action, detail])
|
|
self.conn.commit()
|
|
|
|
def close(self):
|
|
if hasattr(self._local, "conn") and self._local.conn:
|
|
self._local.conn.close()
|
|
self._local.conn = None
|