feat: Implement comprehensive database schema with new models, CRUD operations, and documentation for host metrics, Docker management, and terminal sessions, while removing old test files.
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
This commit is contained in:
@@ -14,9 +14,9 @@ class BootstrapStatus(Base):
|
||||
__tablename__ = "bootstrap_status"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
host_id: Mapped[str] = mapped_column(String, ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False)
|
||||
status: Mapped[str] = mapped_column(String, nullable=False)
|
||||
automation_user: Mapped[str] = mapped_column(String, nullable=True)
|
||||
host_id: Mapped[str] = mapped_column(String(50), ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False)
|
||||
status: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
automation_user: Mapped[str] = mapped_column(String(50), nullable=True)
|
||||
last_attempt: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
error_message: Mapped[str] = mapped_column(Text, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
||||
|
||||
@@ -16,7 +16,7 @@ class ContainerCustomization(Base):
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True)
|
||||
|
||||
host_id: Mapped[str] = mapped_column(String, ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False)
|
||||
host_id: Mapped[str] = mapped_column(String(50), ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False)
|
||||
container_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
|
||||
icon_key: Mapped[str] = mapped_column(String(100), nullable=True)
|
||||
|
||||
+37
-10
@@ -11,7 +11,7 @@ from urllib.parse import urlparse
|
||||
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from sqlalchemy import event, MetaData
|
||||
from sqlalchemy import event, MetaData, text
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.orm import declarative_base
|
||||
|
||||
@@ -115,8 +115,9 @@ async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||
|
||||
async def init_db() -> None:
|
||||
"""Create all tables (mostly for dev/tests; migrations should be handled by Alembic)."""
|
||||
from . import (
|
||||
from . import ( # noqa: F401 — register ALL models with Base.metadata
|
||||
host,
|
||||
host_metrics,
|
||||
task,
|
||||
schedule,
|
||||
schedule_run,
|
||||
@@ -130,10 +131,19 @@ async def init_db() -> None:
|
||||
playbook_lint,
|
||||
favorite_group,
|
||||
favorite_container,
|
||||
) # noqa: F401
|
||||
user,
|
||||
terminal_session,
|
||||
terminal_command_log,
|
||||
bootstrap_status,
|
||||
container_customization,
|
||||
)
|
||||
|
||||
def _to_sync_database_url(db_url: str) -> str:
|
||||
return db_url.replace("sqlite+aiosqlite:", "sqlite:")
|
||||
if db_url.startswith("sqlite+aiosqlite:"):
|
||||
return db_url.replace("sqlite+aiosqlite:", "sqlite:")
|
||||
elif db_url.startswith("mysql+aiomysql:"):
|
||||
return db_url.replace("mysql+aiomysql:", "mysql+pymysql:")
|
||||
return db_url
|
||||
|
||||
def _run_alembic_upgrade() -> None:
|
||||
# Try multiple locations for alembic.ini (dev vs Docker)
|
||||
@@ -155,8 +165,26 @@ async def init_db() -> None:
|
||||
return
|
||||
|
||||
try:
|
||||
# For MySQL, pre-create alembic_version with VARCHAR(255) to prevent
|
||||
# "Data too long for column 'version_num'" error on long revision IDs.
|
||||
# SQLite doesn't enforce VARCHAR length, so this is only needed for MySQL.
|
||||
sync_url = _to_sync_database_url(DATABASE_URL)
|
||||
if "mysql" in sync_url:
|
||||
import sqlalchemy as sa
|
||||
sync_engine = sa.create_engine(sync_url)
|
||||
with sync_engine.connect() as conn:
|
||||
conn.execute(sa.text(
|
||||
"CREATE TABLE IF NOT EXISTS alembic_version "
|
||||
"(version_num VARCHAR(255) NOT NULL, "
|
||||
"PRIMARY KEY (version_num))"
|
||||
))
|
||||
conn.commit()
|
||||
sync_engine.dispose()
|
||||
print("[DB] MySQL: alembic_version table ensured with VARCHAR(255)")
|
||||
|
||||
cfg = Config(str(alembic_ini))
|
||||
cfg.set_main_option("sqlalchemy.url", _to_sync_database_url(DATABASE_URL))
|
||||
cfg.set_main_option("sqlalchemy.url", sync_url)
|
||||
|
||||
print(f"[DB] Running Alembic upgrade to head...")
|
||||
command.upgrade(cfg, "head")
|
||||
print(f"[DB] Alembic upgrade completed successfully")
|
||||
@@ -164,10 +192,9 @@ async def init_db() -> None:
|
||||
print(f"[DB] Alembic upgrade failed: {e}")
|
||||
raise
|
||||
|
||||
try:
|
||||
await asyncio.to_thread(_run_alembic_upgrade)
|
||||
except Exception as e:
|
||||
print(f"[DB] Exception during Alembic migration: {e}")
|
||||
|
||||
print(f"[DB] Initializing database with URL: {DATABASE_URL}")
|
||||
await asyncio.to_thread(_run_alembic_upgrade)
|
||||
print("[DB] Ensuring all tables exist (metadata create_all)...")
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
print("[DB] Database initialization complete.")
|
||||
|
||||
@@ -16,7 +16,7 @@ class DockerAlert(Base):
|
||||
__tablename__ = "docker_alerts"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
host_id: Mapped[str] = mapped_column(String, ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False)
|
||||
host_id: Mapped[str] = mapped_column(String(50), ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False)
|
||||
container_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
severity: Mapped[str] = mapped_column(String(20), nullable=False, default="warning") # warning/error/critical
|
||||
state: Mapped[str] = mapped_column(String(20), nullable=False, default="open") # open/closed/acknowledged
|
||||
|
||||
@@ -16,7 +16,7 @@ class DockerContainer(Base):
|
||||
__tablename__ = "docker_containers"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
host_id: Mapped[str] = mapped_column(String, ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False)
|
||||
host_id: Mapped[str] = mapped_column(String(50), ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False)
|
||||
container_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
image: Mapped[str] = mapped_column(String(255), nullable=True)
|
||||
|
||||
@@ -16,7 +16,7 @@ class DockerImage(Base):
|
||||
__tablename__ = "docker_images"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
host_id: Mapped[str] = mapped_column(String, ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False)
|
||||
host_id: Mapped[str] = mapped_column(String(50), ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False)
|
||||
image_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
repo_tags: Mapped[list] = mapped_column(JSON, nullable=True) # ["nginx:latest", "nginx:1.25"]
|
||||
size: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
||||
|
||||
@@ -16,7 +16,7 @@ class DockerVolume(Base):
|
||||
__tablename__ = "docker_volumes"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
host_id: Mapped[str] = mapped_column(String, ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False)
|
||||
host_id: Mapped[str] = mapped_column(String(50), ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
driver: Mapped[str] = mapped_column(String(50), nullable=True)
|
||||
mountpoint: Mapped[str] = mapped_column(Text, nullable=True)
|
||||
|
||||
+5
-5
@@ -13,11 +13,11 @@ from .database import Base
|
||||
class Host(Base):
|
||||
__tablename__ = "hosts"
|
||||
|
||||
id: Mapped[str] = mapped_column(String, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
ip_address: Mapped[str] = mapped_column(String, nullable=False, unique=True)
|
||||
status: Mapped[str] = mapped_column(String, nullable=False, server_default=text("'unknown'"))
|
||||
ansible_group: Mapped[str] = mapped_column(String, nullable=True)
|
||||
id: Mapped[str] = mapped_column(String(50), primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
ip_address: Mapped[str] = mapped_column(String(50), nullable=False, unique=True)
|
||||
status: Mapped[str] = mapped_column(String(50), nullable=False, server_default=text("'unknown'"))
|
||||
ansible_group: Mapped[str] = mapped_column(String(50), nullable=True)
|
||||
last_seen: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
reachable: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("0"))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
||||
|
||||
@@ -20,7 +20,7 @@ class HostMetrics(Base):
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
host_id: Mapped[str] = mapped_column(String, ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False)
|
||||
host_id: Mapped[str] = mapped_column(String(50), ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False)
|
||||
metric_type: Mapped[str] = mapped_column(String(50), nullable=False) # 'system_info', 'disk_usage', 'memory', etc.
|
||||
|
||||
# Métriques CPU
|
||||
|
||||
+5
-5
@@ -19,13 +19,13 @@ class Log(Base):
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
level: Mapped[str] = mapped_column(String, nullable=False)
|
||||
source: Mapped[str] = mapped_column(String, nullable=True)
|
||||
level: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
source: Mapped[str] = mapped_column(String(255), nullable=True)
|
||||
message: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
details: Mapped[dict] = mapped_column(JSON, nullable=True)
|
||||
host_id: Mapped[str] = mapped_column(String, nullable=True)
|
||||
task_id: Mapped[str] = mapped_column(String, nullable=True)
|
||||
schedule_id: Mapped[str] = mapped_column(String, nullable=True)
|
||||
host_id: Mapped[str] = mapped_column(String(50), nullable=True)
|
||||
task_id: Mapped[str] = mapped_column(String(50), nullable=True)
|
||||
schedule_id: Mapped[str] = mapped_column(String(50), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
||||
|
||||
|
||||
|
||||
+12
-12
@@ -13,31 +13,31 @@ from .database import Base
|
||||
class Schedule(Base):
|
||||
__tablename__ = "schedules"
|
||||
|
||||
id: Mapped[str] = mapped_column(String, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
id: Mapped[str] = mapped_column(String(50), primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
description: Mapped[str] = mapped_column(Text, nullable=True)
|
||||
playbook: Mapped[str] = mapped_column(String, nullable=False)
|
||||
target_type: Mapped[str] = mapped_column(String, default="group", nullable=True)
|
||||
target: Mapped[str] = mapped_column(String, nullable=False)
|
||||
playbook: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
target_type: Mapped[str] = mapped_column(String(50), default="group", nullable=True)
|
||||
target: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
extra_vars: Mapped[Dict[str, Any]] = mapped_column(JSON, nullable=True)
|
||||
schedule_type: Mapped[str] = mapped_column(String, nullable=False)
|
||||
schedule_type: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
schedule_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
recurrence_type: Mapped[str] = mapped_column(String, nullable=True)
|
||||
recurrence_time: Mapped[str] = mapped_column(String, nullable=True)
|
||||
recurrence_type: Mapped[str] = mapped_column(String(50), nullable=True)
|
||||
recurrence_time: Mapped[str] = mapped_column(String(50), nullable=True)
|
||||
recurrence_days: Mapped[str] = mapped_column(Text, nullable=True)
|
||||
cron_expression: Mapped[str] = mapped_column(String, nullable=True)
|
||||
timezone: Mapped[str] = mapped_column(String, default="America/Montreal", nullable=True)
|
||||
cron_expression: Mapped[str] = mapped_column(String(100), nullable=True)
|
||||
timezone: Mapped[str] = mapped_column(String(100), default="America/Montreal", nullable=True)
|
||||
start_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
end_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
tags: Mapped[str] = mapped_column(Text, nullable=True)
|
||||
next_run: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
last_run: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
last_status: Mapped[str] = mapped_column(String, default="never", nullable=True)
|
||||
last_status: Mapped[str] = mapped_column(String(50), default="never", nullable=True)
|
||||
retry_on_failure: Mapped[int] = mapped_column(Integer, default=0, nullable=True)
|
||||
timeout: Mapped[int] = mapped_column(Integer, default=3600, nullable=True)
|
||||
# Type de notification: "none" (aucune), "all" (toujours), "errors" (erreurs seulement)
|
||||
notification_type: Mapped[str] = mapped_column(String, default="all", nullable=True)
|
||||
notification_type: Mapped[str] = mapped_column(String(50), default="all", nullable=True)
|
||||
run_count: Mapped[int] = mapped_column(Integer, default=0, nullable=True)
|
||||
success_count: Mapped[int] = mapped_column(Integer, default=0, nullable=True)
|
||||
failure_count: Mapped[int] = mapped_column(Integer, default=0, nullable=True)
|
||||
|
||||
@@ -14,9 +14,9 @@ class ScheduleRun(Base):
|
||||
__tablename__ = "schedule_runs"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
schedule_id: Mapped[str] = mapped_column(String, ForeignKey("schedules.id", ondelete="CASCADE"), nullable=False)
|
||||
task_id: Mapped[str] = mapped_column(String, ForeignKey("tasks.id", ondelete="SET NULL"), nullable=True)
|
||||
status: Mapped[str] = mapped_column(String, nullable=False)
|
||||
schedule_id: Mapped[str] = mapped_column(String(50), ForeignKey("schedules.id", ondelete="CASCADE"), nullable=False)
|
||||
task_id: Mapped[str] = mapped_column(String(50), ForeignKey("tasks.id", ondelete="SET NULL"), nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
completed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
duration: Mapped[float] = mapped_column(Float, nullable=True)
|
||||
|
||||
+5
-5
@@ -13,11 +13,11 @@ from .database import Base
|
||||
class Task(Base):
|
||||
__tablename__ = "tasks"
|
||||
|
||||
id: Mapped[str] = mapped_column(String, primary_key=True)
|
||||
action: Mapped[str] = mapped_column(String, nullable=False)
|
||||
target: Mapped[str] = mapped_column(String, nullable=False)
|
||||
status: Mapped[str] = mapped_column(String, nullable=False, server_default=text("'pending'"))
|
||||
playbook: Mapped[str] = mapped_column(String, nullable=True)
|
||||
id: Mapped[str] = mapped_column(String(50), primary_key=True)
|
||||
action: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
target: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
status: Mapped[str] = mapped_column(String(50), nullable=False, server_default=text("'pending'"))
|
||||
playbook: Mapped[str] = mapped_column(String(255), nullable=True)
|
||||
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
completed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
error_message: Mapped[str] = mapped_column(Text, nullable=True)
|
||||
|
||||
@@ -36,10 +36,10 @@ class TerminalCommandLog(Base):
|
||||
|
||||
# Foreign keys
|
||||
host_id: Mapped[str] = mapped_column(
|
||||
String, ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
String(50), ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
user_id: Mapped[str] = mapped_column(
|
||||
String, ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True
|
||||
user_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True
|
||||
)
|
||||
|
||||
# Session reference (not FK as sessions may be cleaned up)
|
||||
|
||||
@@ -53,12 +53,12 @@ class TerminalSession(Base):
|
||||
Index('ix_terminal_sessions_last_seen', 'last_seen_at'),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
host_id: Mapped[str] = mapped_column(String, nullable=False, index=True)
|
||||
host_name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
host_ip: Mapped[str] = mapped_column(String, nullable=False)
|
||||
user_id: Mapped[str] = mapped_column(String, nullable=True)
|
||||
username: Mapped[str] = mapped_column(String, nullable=True)
|
||||
id: Mapped[str] = mapped_column(String(50), primary_key=True)
|
||||
host_id: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
|
||||
host_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
host_ip: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
user_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
||||
username: Mapped[str] = mapped_column(String(50), nullable=True)
|
||||
|
||||
# Token hash for session authentication (never store plain token)
|
||||
token_hash: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
|
||||
Reference in New Issue
Block a user